1 // 4.1.6 (2014-10-08) 2 3 /** 4 * Compiled inline version. (Library mode) 5 */ 6 7 /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ 8 /*globals $code */ 9 10 (function(exports, undefined) { 11 "use strict"; 12 13 var modules = {}; 14 15 function require(ids, callback) { 16 var module, defs = []; 17 18 for (var i = 0; i < ids.length; ++i) { 19 module = modules[ids[i]] || resolve(ids[i]); 20 if (!module) { 21 throw 'module definition dependecy not found: ' + ids[i]; 22 } 23 24 defs.push(module); 25 } 26 27 callback.apply(null, defs); 28 } 29 30 function define(id, dependencies, definition) { 31 if (typeof id !== 'string') { 32 throw 'invalid module definition, module id must be defined and be a string'; 33 } 34 35 if (dependencies === undefined) { 36 throw 'invalid module definition, dependencies must be specified'; 37 } 38 39 if (definition === undefined) { 40 throw 'invalid module definition, definition function must be specified'; 41 } 42 43 require(dependencies, function() { 44 modules[id] = definition.apply(null, arguments); 45 }); 46 } 47 48 function defined(id) { 49 return !!modules[id]; 50 } 51 52 function resolve(id) { 53 var target = exports; 54 var fragments = id.split(/[.\/]/); 55 56 for (var fi = 0; fi < fragments.length; ++fi) { 57 if (!target[fragments[fi]]) { 58 return; 59 } 60 61 target = target[fragments[fi]]; 62 } 63 64 return target; 65 } 66 67 function expose(ids) { 68 for (var i = 0; i < ids.length; i++) { 69 var target = exports; 70 var id = ids[i]; 71 var fragments = id.split(/[.\/]/); 72 73 for (var fi = 0; fi < fragments.length - 1; ++fi) { 74 if (target[fragments[fi]] === undefined) { 75 target[fragments[fi]] = {}; 76 } 77 78 target = target[fragments[fi]]; 79 } 80 81 target[fragments[fragments.length - 1]] = modules[id]; 82 } 83 } 84 85 // Included from: js/tinymce/classes/dom/EventUtils.js 86 87 /** 88 * EventUtils.js 89 * 90 * Copyright, Moxiecode Systems AB 91 * Released under LGPL License. 92 * 93 * License: http://www.tinymce.com/license 94 * Contributing: http://www.tinymce.com/contributing 95 */ 96 97 /*jshint loopfunc:true*/ 98 /*eslint no-loop-func:0 */ 99 100 /** 101 * This class wraps the browsers native event logic with more convenient methods. 102 * 103 * @class tinymce.dom.EventUtils 104 */ 105 define("tinymce/dom/EventUtils", [], function() { 106 "use strict"; 107 108 var eventExpandoPrefix = "mce-data-"; 109 var mouseEventRe = /^(?:mouse|contextmenu)|click/; 110 var deprecated = {keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1}; 111 112 /** 113 * Binds a native event to a callback on the speified target. 114 */ 115 function addEvent(target, name, callback, capture) { 116 if (target.addEventListener) { 117 target.addEventListener(name, callback, capture || false); 118 } else if (target.attachEvent) { 119 target.attachEvent('on' + name, callback); 120 } 121 } 122 123 /** 124 * Unbinds a native event callback on the specified target. 125 */ 126 function removeEvent(target, name, callback, capture) { 127 if (target.removeEventListener) { 128 target.removeEventListener(name, callback, capture || false); 129 } else if (target.detachEvent) { 130 target.detachEvent('on' + name, callback); 131 } 132 } 133 134 /** 135 * Normalizes a native event object or just adds the event specific methods on a custom event. 136 */ 137 function fix(originalEvent, data) { 138 var name, event = data || {}, undef; 139 140 // Dummy function that gets replaced on the delegation state functions 141 function returnFalse() { 142 return false; 143 } 144 145 // Dummy function that gets replaced on the delegation state functions 146 function returnTrue() { 147 return true; 148 } 149 150 // Copy all properties from the original event 151 for (name in originalEvent) { 152 // layerX/layerY is deprecated in Chrome and produces a warning 153 if (!deprecated[name]) { 154 event[name] = originalEvent[name]; 155 } 156 } 157 158 // Normalize target IE uses srcElement 159 if (!event.target) { 160 event.target = event.srcElement || document; 161 } 162 163 // Calculate pageX/Y if missing and clientX/Y available 164 if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) { 165 var eventDoc = event.target.ownerDocument || document; 166 var doc = eventDoc.documentElement; 167 var body = eventDoc.body; 168 169 event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - 170 (doc && doc.clientLeft || body && body.clientLeft || 0); 171 172 event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - 173 (doc && doc.clientTop || body && body.clientTop || 0); 174 } 175 176 // Add preventDefault method 177 event.preventDefault = function() { 178 event.isDefaultPrevented = returnTrue; 179 180 // Execute preventDefault on the original event object 181 if (originalEvent) { 182 if (originalEvent.preventDefault) { 183 originalEvent.preventDefault(); 184 } else { 185 originalEvent.returnValue = false; // IE 186 } 187 } 188 }; 189 190 // Add stopPropagation 191 event.stopPropagation = function() { 192 event.isPropagationStopped = returnTrue; 193 194 // Execute stopPropagation on the original event object 195 if (originalEvent) { 196 if (originalEvent.stopPropagation) { 197 originalEvent.stopPropagation(); 198 } else { 199 originalEvent.cancelBubble = true; // IE 200 } 201 } 202 }; 203 204 // Add stopImmediatePropagation 205 event.stopImmediatePropagation = function() { 206 event.isImmediatePropagationStopped = returnTrue; 207 event.stopPropagation(); 208 }; 209 210 // Add event delegation states 211 if (!event.isDefaultPrevented) { 212 event.isDefaultPrevented = returnFalse; 213 event.isPropagationStopped = returnFalse; 214 event.isImmediatePropagationStopped = returnFalse; 215 } 216 217 return event; 218 } 219 220 /** 221 * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized. 222 * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times. 223 */ 224 function bindOnReady(win, callback, eventUtils) { 225 var doc = win.document, event = {type: 'ready'}; 226 227 if (eventUtils.domLoaded) { 228 callback(event); 229 return; 230 } 231 232 // Gets called when the DOM is ready 233 function readyHandler() { 234 if (!eventUtils.domLoaded) { 235 eventUtils.domLoaded = true; 236 callback(event); 237 } 238 } 239 240 function waitForDomLoaded() { 241 // Check complete or interactive state if there is a body 242 // element on some iframes IE 8 will produce a null body 243 if (doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body)) { 244 removeEvent(doc, "readystatechange", waitForDomLoaded); 245 readyHandler(); 246 } 247 } 248 249 function tryScroll() { 250 try { 251 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. 252 // http://javascript.nwbox.com/IEContentLoaded/ 253 doc.documentElement.doScroll("left"); 254 } catch (ex) { 255 setTimeout(tryScroll, 0); 256 return; 257 } 258 259 readyHandler(); 260 } 261 262 // Use W3C method 263 if (doc.addEventListener) { 264 if (doc.readyState === "complete") { 265 readyHandler(); 266 } else { 267 addEvent(win, 'DOMContentLoaded', readyHandler); 268 } 269 } else { 270 // Use IE method 271 addEvent(doc, "readystatechange", waitForDomLoaded); 272 273 // Wait until we can scroll, when we can the DOM is initialized 274 if (doc.documentElement.doScroll && win.self === win.top) { 275 tryScroll(); 276 } 277 } 278 279 // Fallback if any of the above methods should fail for some odd reason 280 addEvent(win, 'load', readyHandler); 281 } 282 283 /** 284 * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers. 285 */ 286 function EventUtils() { 287 var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; 288 289 expando = eventExpandoPrefix + (+new Date()).toString(32); 290 hasMouseEnterLeave = "onmouseenter" in document.documentElement; 291 hasFocusIn = "onfocusin" in document.documentElement; 292 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; 293 count = 1; 294 295 // State if the DOMContentLoaded was executed or not 296 self.domLoaded = false; 297 self.events = events; 298 299 /** 300 * Executes all event handler callbacks for a specific event. 301 * 302 * @private 303 * @param {Event} evt Event object. 304 * @param {String} id Expando id value to look for. 305 */ 306 function executeHandlers(evt, id) { 307 var callbackList, i, l, callback, container = events[id]; 308 309 callbackList = container && container[evt.type]; 310 if (callbackList) { 311 for (i = 0, l = callbackList.length; i < l; i++) { 312 callback = callbackList[i]; 313 314 // Check if callback exists might be removed if a unbind is called inside the callback 315 if (callback && callback.func.call(callback.scope, evt) === false) { 316 evt.preventDefault(); 317 } 318 319 // Should we stop propagation to immediate listeners 320 if (evt.isImmediatePropagationStopped()) { 321 return; 322 } 323 } 324 } 325 } 326 327 /** 328 * Binds a callback to an event on the specified target. 329 * 330 * @method bind 331 * @param {Object} target Target node/window or custom object. 332 * @param {String} names Name of the event to bind. 333 * @param {function} callback Callback function to execute when the event occurs. 334 * @param {Object} scope Scope to call the callback function on, defaults to target. 335 * @return {function} Callback function that got bound. 336 */ 337 self.bind = function(target, names, callback, scope) { 338 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; 339 340 // Native event handler function patches the event and executes the callbacks for the expando 341 function defaultNativeHandler(evt) { 342 executeHandlers(fix(evt || win.event), id); 343 } 344 345 // Don't bind to text nodes or comments 346 if (!target || target.nodeType === 3 || target.nodeType === 8) { 347 return; 348 } 349 350 // Create or get events id for the target 351 if (!target[expando]) { 352 id = count++; 353 target[expando] = id; 354 events[id] = {}; 355 } else { 356 id = target[expando]; 357 } 358 359 // Setup the specified scope or use the target as a default 360 scope = scope || target; 361 362 // Split names and bind each event, enables you to bind multiple events with one call 363 names = names.split(' '); 364 i = names.length; 365 while (i--) { 366 name = names[i]; 367 nativeHandler = defaultNativeHandler; 368 fakeName = capture = false; 369 370 // Use ready instead of DOMContentLoaded 371 if (name === "DOMContentLoaded") { 372 name = "ready"; 373 } 374 375 // DOM is already ready 376 if (self.domLoaded && name === "ready" && target.readyState == 'complete') { 377 callback.call(scope, fix({type: name})); 378 continue; 379 } 380 381 // Handle mouseenter/mouseleaver 382 if (!hasMouseEnterLeave) { 383 fakeName = mouseEnterLeave[name]; 384 385 if (fakeName) { 386 nativeHandler = function(evt) { 387 var current, related; 388 389 current = evt.currentTarget; 390 related = evt.relatedTarget; 391 392 // Check if related is inside the current target if it's not then the event should 393 // be ignored since it's a mouseover/mouseout inside the element 394 if (related && current.contains) { 395 // Use contains for performance 396 related = current.contains(related); 397 } else { 398 while (related && related !== current) { 399 related = related.parentNode; 400 } 401 } 402 403 // Fire fake event 404 if (!related) { 405 evt = fix(evt || win.event); 406 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; 407 evt.target = current; 408 executeHandlers(evt, id); 409 } 410 }; 411 } 412 } 413 414 // Fake bubbeling of focusin/focusout 415 if (!hasFocusIn && (name === "focusin" || name === "focusout")) { 416 capture = true; 417 fakeName = name === "focusin" ? "focus" : "blur"; 418 nativeHandler = function(evt) { 419 evt = fix(evt || win.event); 420 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; 421 executeHandlers(evt, id); 422 }; 423 } 424 425 // Setup callback list and bind native event 426 callbackList = events[id][name]; 427 if (!callbackList) { 428 events[id][name] = callbackList = [{func: callback, scope: scope}]; 429 callbackList.fakeName = fakeName; 430 callbackList.capture = capture; 431 //callbackList.callback = callback; 432 433 // Add the nativeHandler to the callback list so that we can later unbind it 434 callbackList.nativeHandler = nativeHandler; 435 436 // Check if the target has native events support 437 438 if (name === "ready") { 439 bindOnReady(target, nativeHandler, self); 440 } else { 441 addEvent(target, fakeName || name, nativeHandler, capture); 442 } 443 } else { 444 if (name === "ready" && self.domLoaded) { 445 callback({type: name}); 446 } else { 447 // If it already has an native handler then just push the callback 448 callbackList.push({func: callback, scope: scope}); 449 } 450 } 451 } 452 453 target = callbackList = 0; // Clean memory for IE 454 455 return callback; 456 }; 457 458 /** 459 * Unbinds the specified event by name, name and callback or all events on the target. 460 * 461 * @method unbind 462 * @param {Object} target Target node/window or custom object. 463 * @param {String} names Optional event name to unbind. 464 * @param {function} callback Optional callback function to unbind. 465 * @return {EventUtils} Event utils instance. 466 */ 467 self.unbind = function(target, names, callback) { 468 var id, callbackList, i, ci, name, eventMap; 469 470 // Don't bind to text nodes or comments 471 if (!target || target.nodeType === 3 || target.nodeType === 8) { 472 return self; 473 } 474 475 // Unbind event or events if the target has the expando 476 id = target[expando]; 477 if (id) { 478 eventMap = events[id]; 479 480 // Specific callback 481 if (names) { 482 names = names.split(' '); 483 i = names.length; 484 while (i--) { 485 name = names[i]; 486 callbackList = eventMap[name]; 487 488 // Unbind the event if it exists in the map 489 if (callbackList) { 490 // Remove specified callback 491 if (callback) { 492 ci = callbackList.length; 493 while (ci--) { 494 if (callbackList[ci].func === callback) { 495 var nativeHandler = callbackList.nativeHandler; 496 var fakeName = callbackList.fakeName, capture = callbackList.capture; 497 498 // Clone callbackList since unbind inside a callback would otherwise break the handlers loop 499 callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1)); 500 callbackList.nativeHandler = nativeHandler; 501 callbackList.fakeName = fakeName; 502 callbackList.capture = capture; 503 504 eventMap[name] = callbackList; 505 } 506 } 507 } 508 509 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 510 if (!callback || callbackList.length === 0) { 511 delete eventMap[name]; 512 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 513 } 514 } 515 } 516 } else { 517 // All events for a specific element 518 for (name in eventMap) { 519 callbackList = eventMap[name]; 520 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 521 } 522 523 eventMap = {}; 524 } 525 526 // Check if object is empty, if it isn't then we won't remove the expando map 527 for (name in eventMap) { 528 return self; 529 } 530 531 // Delete event object 532 delete events[id]; 533 534 // Remove expando from target 535 try { 536 // IE will fail here since it can't delete properties from window 537 delete target[expando]; 538 } catch (ex) { 539 // IE will set it to null 540 target[expando] = null; 541 } 542 } 543 544 return self; 545 }; 546 547 /** 548 * Fires the specified event on the specified target. 549 * 550 * @method fire 551 * @param {Object} target Target node/window or custom object. 552 * @param {String} name Event name to fire. 553 * @param {Object} args Optional arguments to send to the observers. 554 * @return {EventUtils} Event utils instance. 555 */ 556 self.fire = function(target, name, args) { 557 var id; 558 559 // Don't bind to text nodes or comments 560 if (!target || target.nodeType === 3 || target.nodeType === 8) { 561 return self; 562 } 563 564 // Build event object by patching the args 565 args = fix(null, args); 566 args.type = name; 567 args.target = target; 568 569 do { 570 // Found an expando that means there is listeners to execute 571 id = target[expando]; 572 if (id) { 573 executeHandlers(args, id); 574 } 575 576 // Walk up the DOM 577 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 578 } while (target && !args.isPropagationStopped()); 579 580 return self; 581 }; 582 583 /** 584 * Removes all bound event listeners for the specified target. This will also remove any bound 585 * listeners to child nodes within that target. 586 * 587 * @method clean 588 * @param {Object} target Target node/window object. 589 * @return {EventUtils} Event utils instance. 590 */ 591 self.clean = function(target) { 592 var i, children, unbind = self.unbind; 593 594 // Don't bind to text nodes or comments 595 if (!target || target.nodeType === 3 || target.nodeType === 8) { 596 return self; 597 } 598 599 // Unbind any element on the specificed target 600 if (target[expando]) { 601 unbind(target); 602 } 603 604 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 605 if (!target.getElementsByTagName) { 606 target = target.document; 607 } 608 609 // Remove events from each child element 610 if (target && target.getElementsByTagName) { 611 unbind(target); 612 613 children = target.getElementsByTagName('*'); 614 i = children.length; 615 while (i--) { 616 target = children[i]; 617 618 if (target[expando]) { 619 unbind(target); 620 } 621 } 622 } 623 624 return self; 625 }; 626 627 /** 628 * Destroys the event object. Call this on IE to remove memory leaks. 629 */ 630 self.destroy = function() { 631 events = {}; 632 }; 633 634 // Legacy function for canceling events 635 self.cancel = function(e) { 636 if (e) { 637 e.preventDefault(); 638 e.stopImmediatePropagation(); 639 } 640 641 return false; 642 }; 643 } 644 645 EventUtils.Event = new EventUtils(); 646 EventUtils.Event.bind(window, 'ready', function() {}); 647 648 return EventUtils; 649 }); 650 651 // Included from: js/tinymce/classes/dom/Sizzle.jQuery.js 652 653 /** 654 * Sizzle.jQuery.js 655 * 656 * Copyright, Moxiecode Systems AB 657 * Released under LGPL License. 658 * 659 * License: http://www.tinymce.com/license 660 * Contributing: http://www.tinymce.com/contributing 661 */ 662 663 /*global jQuery:true */ 664 665 /* 666 * Fake Sizzle using jQuery. 667 */ 668 define("tinymce/dom/Sizzle", [], function() { 669 // Detect if jQuery is loaded 670 if (!window.jQuery) { 671 throw new Error("Load jQuery first"); 672 } 673 674 return jQuery.find; 675 }); 676 677 // Included from: js/tinymce/classes/util/Tools.js 678 679 /** 680 * Tools.js 681 * 682 * Copyright, Moxiecode Systems AB 683 * Released under LGPL License. 684 * 685 * License: http://www.tinymce.com/license 686 * Contributing: http://www.tinymce.com/contributing 687 */ 688 689 /** 690 * This class contains various utlity functions. These are also exposed 691 * directly on the tinymce namespace. 692 * 693 * @class tinymce.util.Tools 694 */ 695 define("tinymce/util/Tools", [], function() { 696 /** 697 * Removes whitespace from the beginning and end of a string. 698 * 699 * @method trim 700 * @param {String} s String to remove whitespace from. 701 * @return {String} New string with removed whitespace. 702 */ 703 var whiteSpaceRegExp = /^\s*|\s*$/g; 704 705 function trim(str) { 706 return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, ''); 707 } 708 709 /** 710 * Returns true/false if the object is an array or not. 711 * 712 * @method isArray 713 * @param {Object} obj Object to check. 714 * @return {boolean} true/false state if the object is an array or not. 715 */ 716 var isArray = Array.isArray || function(obj) { 717 return Object.prototype.toString.call(obj) === "[object Array]"; 718 }; 719 720 /** 721 * Checks if a object is of a specific type for example an array. 722 * 723 * @method is 724 * @param {Object} o Object to check type of. 725 * @param {string} t Optional type to check for. 726 * @return {Boolean} true/false if the object is of the specified type. 727 */ 728 function is(o, t) { 729 if (!t) { 730 return o !== undefined; 731 } 732 733 if (t == 'array' && isArray(o)) { 734 return true; 735 } 736 737 return typeof(o) == t; 738 } 739 740 /** 741 * Converts the specified object into a real JavaScript array. 742 * 743 * @method toArray 744 * @param {Object} obj Object to convert into array. 745 * @return {Array} Array object based in input. 746 */ 747 function toArray(obj) { 748 var array = obj, i, l; 749 750 if (!isArray(obj)) { 751 array = []; 752 for (i = 0, l = obj.length; i < l; i++) { 753 array[i] = obj[i]; 754 } 755 } 756 757 return array; 758 } 759 760 /** 761 * Makes a name/object map out of an array with names. 762 * 763 * @method makeMap 764 * @param {Array/String} items Items to make map out of. 765 * @param {String} delim Optional delimiter to split string by. 766 * @param {Object} map Optional map to add items to. 767 * @return {Object} Name/value map of items. 768 */ 769 function makeMap(items, delim, map) { 770 var i; 771 772 items = items || []; 773 delim = delim || ','; 774 775 if (typeof(items) == "string") { 776 items = items.split(delim); 777 } 778 779 map = map || {}; 780 781 i = items.length; 782 while (i--) { 783 map[items[i]] = {}; 784 } 785 786 return map; 787 } 788 789 /** 790 * Performs an iteration of all items in a collection such as an object or array. This method will execure the 791 * callback function for each item in the collection, if the callback returns false the iteration will terminate. 792 * The callback has the following format: cb(value, key_or_index). 793 * 794 * @method each 795 * @param {Object} o Collection to iterate. 796 * @param {function} cb Callback function to execute for each item. 797 * @param {Object} s Optional scope to execute the callback in. 798 * @example 799 * // Iterate an array 800 * tinymce.each([1,2,3], function(v, i) { 801 * console.debug("Value: " + v + ", Index: " + i); 802 * }); 803 * 804 * // Iterate an object 805 * tinymce.each({a: 1, b: 2, c: 3], function(v, k) { 806 * console.debug("Value: " + v + ", Key: " + k); 807 * }); 808 */ 809 function each(o, cb, s) { 810 var n, l; 811 812 if (!o) { 813 return 0; 814 } 815 816 s = s || o; 817 818 if (o.length !== undefined) { 819 // Indexed arrays, needed for Safari 820 for (n = 0, l = o.length; n < l; n++) { 821 if (cb.call(s, o[n], n, o) === false) { 822 return 0; 823 } 824 } 825 } else { 826 // Hashtables 827 for (n in o) { 828 if (o.hasOwnProperty(n)) { 829 if (cb.call(s, o[n], n, o) === false) { 830 return 0; 831 } 832 } 833 } 834 } 835 836 return 1; 837 } 838 839 /** 840 * Creates a new array by the return value of each iteration function call. This enables you to convert 841 * one array list into another. 842 * 843 * @method map 844 * @param {Array} a Array of items to iterate. 845 * @param {function} f Function to call for each item. It's return value will be the new value. 846 * @return {Array} Array with new values based on function return values. 847 */ 848 function map(a, f) { 849 var o = []; 850 851 each(a, function(v) { 852 o.push(f(v)); 853 }); 854 855 return o; 856 } 857 858 /** 859 * Filters out items from the input array by calling the specified function for each item. 860 * If the function returns false the item will be excluded if it returns true it will be included. 861 * 862 * @method grep 863 * @param {Array} a Array of items to loop though. 864 * @param {function} f Function to call for each item. Include/exclude depends on it's return value. 865 * @return {Array} New array with values imported and filtered based in input. 866 * @example 867 * // Filter out some items, this will return an array with 4 and 5 868 * var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;}); 869 */ 870 function grep(a, f) { 871 var o = []; 872 873 each(a, function(v) { 874 if (!f || f(v)) { 875 o.push(v); 876 } 877 }); 878 879 return o; 880 } 881 882 /** 883 * Creates a class, subclass or static singleton. 884 * More details on this method can be found in the Wiki. 885 * 886 * @method create 887 * @param {String} s Class name, inheritage and prefix. 888 * @param {Object} p Collection of methods to add to the class. 889 * @param {Object} root Optional root object defaults to the global window object. 890 * @example 891 * // Creates a basic class 892 * tinymce.create('tinymce.somepackage.SomeClass', { 893 * SomeClass: function() { 894 * // Class constructor 895 * }, 896 * 897 * method: function() { 898 * // Some method 899 * } 900 * }); 901 * 902 * // Creates a basic subclass class 903 * tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', { 904 * SomeSubClass: function() { 905 * // Class constructor 906 * this.parent(); // Call parent constructor 907 * }, 908 * 909 * method: function() { 910 * // Some method 911 * this.parent(); // Call parent method 912 * }, 913 * 914 * 'static': { 915 * staticMethod: function() { 916 * // Static method 917 * } 918 * } 919 * }); 920 * 921 * // Creates a singleton/static class 922 * tinymce.create('static tinymce.somepackage.SomeSingletonClass', { 923 * method: function() { 924 * // Some method 925 * } 926 * }); 927 */ 928 function create(s, p, root) { 929 var self = this, sp, ns, cn, scn, c, de = 0; 930 931 // Parse : <prefix> <class>:<super class> 932 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); 933 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name 934 935 // Create namespace for new class 936 ns = self.createNS(s[3].replace(/\.\w+$/, ''), root); 937 938 // Class already exists 939 if (ns[cn]) { 940 return; 941 } 942 943 // Make pure static class 944 if (s[2] == 'static') { 945 ns[cn] = p; 946 947 if (this.onCreate) { 948 this.onCreate(s[2], s[3], ns[cn]); 949 } 950 951 return; 952 } 953 954 // Create default constructor 955 if (!p[cn]) { 956 p[cn] = function() {}; 957 de = 1; 958 } 959 960 // Add constructor and methods 961 ns[cn] = p[cn]; 962 self.extend(ns[cn].prototype, p); 963 964 // Extend 965 if (s[5]) { 966 sp = self.resolve(s[5]).prototype; 967 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name 968 969 // Extend constructor 970 c = ns[cn]; 971 if (de) { 972 // Add passthrough constructor 973 ns[cn] = function() { 974 return sp[scn].apply(this, arguments); 975 }; 976 } else { 977 // Add inherit constructor 978 ns[cn] = function() { 979 this.parent = sp[scn]; 980 return c.apply(this, arguments); 981 }; 982 } 983 ns[cn].prototype[cn] = ns[cn]; 984 985 // Add super methods 986 self.each(sp, function(f, n) { 987 ns[cn].prototype[n] = sp[n]; 988 }); 989 990 // Add overridden methods 991 self.each(p, function(f, n) { 992 // Extend methods if needed 993 if (sp[n]) { 994 ns[cn].prototype[n] = function() { 995 this.parent = sp[n]; 996 return f.apply(this, arguments); 997 }; 998 } else { 999 if (n != cn) { 1000 ns[cn].prototype[n] = f; 1001 } 1002 } 1003 }); 1004 } 1005 1006 // Add static methods 1007 /*jshint sub:true*/ 1008 self.each(p['static'], function(f, n) { 1009 ns[cn][n] = f; 1010 }); 1011 } 1012 1013 /** 1014 * Returns the index of a value in an array, this method will return -1 if the item wasn't found. 1015 * 1016 * @method inArray 1017 * @param {Array} a Array/Object to search for value in. 1018 * @param {Object} v Value to check for inside the array. 1019 * @return {Number/String} Index of item inside the array inside an object. Or -1 if it wasn't found. 1020 * @example 1021 * // Get index of value in array this will alert 1 since 2 is at that index 1022 * alert(tinymce.inArray([1,2,3], 2)); 1023 */ 1024 function inArray(a, v) { 1025 var i, l; 1026 1027 if (a) { 1028 for (i = 0, l = a.length; i < l; i++) { 1029 if (a[i] === v) { 1030 return i; 1031 } 1032 } 1033 } 1034 1035 return -1; 1036 } 1037 1038 function extend(obj, ext) { 1039 var i, l, name, args = arguments, value; 1040 1041 for (i = 1, l = args.length; i < l; i++) { 1042 ext = args[i]; 1043 for (name in ext) { 1044 if (ext.hasOwnProperty(name)) { 1045 value = ext[name]; 1046 1047 if (value !== undefined) { 1048 obj[name] = value; 1049 } 1050 } 1051 } 1052 } 1053 1054 return obj; 1055 } 1056 1057 /** 1058 * Executed the specified function for each item in a object tree. 1059 * 1060 * @method walk 1061 * @param {Object} o Object tree to walk though. 1062 * @param {function} f Function to call for each item. 1063 * @param {String} n Optional name of collection inside the objects to walk for example childNodes. 1064 * @param {String} s Optional scope to execute the function in. 1065 */ 1066 function walk(o, f, n, s) { 1067 s = s || this; 1068 1069 if (o) { 1070 if (n) { 1071 o = o[n]; 1072 } 1073 1074 each(o, function(o, i) { 1075 if (f.call(s, o, i, n) === false) { 1076 return false; 1077 } 1078 1079 walk(o, f, n, s); 1080 }); 1081 } 1082 } 1083 1084 /** 1085 * Creates a namespace on a specific object. 1086 * 1087 * @method createNS 1088 * @param {String} n Namespace to create for example a.b.c.d. 1089 * @param {Object} o Optional object to add namespace to, defaults to window. 1090 * @return {Object} New namespace object the last item in path. 1091 * @example 1092 * // Create some namespace 1093 * tinymce.createNS('tinymce.somepackage.subpackage'); 1094 * 1095 * // Add a singleton 1096 * var tinymce.somepackage.subpackage.SomeSingleton = { 1097 * method: function() { 1098 * // Some method 1099 * } 1100 * }; 1101 */ 1102 function createNS(n, o) { 1103 var i, v; 1104 1105 o = o || window; 1106 1107 n = n.split('.'); 1108 for (i = 0; i < n.length; i++) { 1109 v = n[i]; 1110 1111 if (!o[v]) { 1112 o[v] = {}; 1113 } 1114 1115 o = o[v]; 1116 } 1117 1118 return o; 1119 } 1120 1121 /** 1122 * Resolves a string and returns the object from a specific structure. 1123 * 1124 * @method resolve 1125 * @param {String} n Path to resolve for example a.b.c.d. 1126 * @param {Object} o Optional object to search though, defaults to window. 1127 * @return {Object} Last object in path or null if it couldn't be resolved. 1128 * @example 1129 * // Resolve a path into an object reference 1130 * var obj = tinymce.resolve('a.b.c.d'); 1131 */ 1132 function resolve(n, o) { 1133 var i, l; 1134 1135 o = o || window; 1136 1137 n = n.split('.'); 1138 for (i = 0, l = n.length; i < l; i++) { 1139 o = o[n[i]]; 1140 1141 if (!o) { 1142 break; 1143 } 1144 } 1145 1146 return o; 1147 } 1148 1149 /** 1150 * Splits a string but removes the whitespace before and after each value. 1151 * 1152 * @method explode 1153 * @param {string} s String to split. 1154 * @param {string} d Delimiter to split by. 1155 * @example 1156 * // Split a string into an array with a,b,c 1157 * var arr = tinymce.explode('a, b, c'); 1158 */ 1159 function explode(s, d) { 1160 if (!s || is(s, 'array')) { 1161 return s; 1162 } 1163 1164 return map(s.split(d || ','), trim); 1165 } 1166 1167 return { 1168 trim: trim, 1169 isArray: isArray, 1170 is: is, 1171 toArray: toArray, 1172 makeMap: makeMap, 1173 each: each, 1174 map: map, 1175 grep: grep, 1176 inArray: inArray, 1177 extend: extend, 1178 create: create, 1179 walk: walk, 1180 createNS: createNS, 1181 resolve: resolve, 1182 explode: explode 1183 }; 1184 }); 1185 1186 // Included from: js/tinymce/classes/Env.js 1187 1188 /** 1189 * Env.js 1190 * 1191 * Copyright, Moxiecode Systems AB 1192 * Released under LGPL License. 1193 * 1194 * License: http://www.tinymce.com/license 1195 * Contributing: http://www.tinymce.com/contributing 1196 */ 1197 1198 /** 1199 * This class contains various environment constants like browser versions etc. 1200 * Normally you don't want to sniff specific browser versions but sometimes you have 1201 * to when it's impossible to feature detect. So use this with care. 1202 * 1203 * @class tinymce.Env 1204 * @static 1205 */ 1206 define("tinymce/Env", [], function() { 1207 var nav = navigator, userAgent = nav.userAgent; 1208 var opera, webkit, ie, ie11, gecko, mac, iDevice; 1209 1210 opera = window.opera && window.opera.buildNumber; 1211 webkit = /WebKit/.test(userAgent); 1212 ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName); 1213 ie = ie && /MSIE (\w+)\./.exec(userAgent)[1]; 1214 ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false; 1215 ie = ie || ie11; 1216 gecko = !webkit && !ie11 && /Gecko/.test(userAgent); 1217 mac = userAgent.indexOf('Mac') != -1; 1218 iDevice = /(iPad|iPhone)/.test(userAgent); 1219 1220 // Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions 1221 // says it has contentEditable support but there is no visible caret. 1222 var contentEditable = !iDevice || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534; 1223 1224 return { 1225 /** 1226 * Constant that is true if the browser is Opera. 1227 * 1228 * @property opera 1229 * @type Boolean 1230 * @final 1231 */ 1232 opera: opera, 1233 1234 /** 1235 * Constant that is true if the browser is WebKit (Safari/Chrome). 1236 * 1237 * @property webKit 1238 * @type Boolean 1239 * @final 1240 */ 1241 webkit: webkit, 1242 1243 /** 1244 * Constant that is more than zero if the browser is IE. 1245 * 1246 * @property ie 1247 * @type Boolean 1248 * @final 1249 */ 1250 ie: ie, 1251 1252 /** 1253 * Constant that is true if the browser is Gecko. 1254 * 1255 * @property gecko 1256 * @type Boolean 1257 * @final 1258 */ 1259 gecko: gecko, 1260 1261 /** 1262 * Constant that is true if the os is Mac OS. 1263 * 1264 * @property mac 1265 * @type Boolean 1266 * @final 1267 */ 1268 mac: mac, 1269 1270 /** 1271 * Constant that is true if the os is iOS. 1272 * 1273 * @property iOS 1274 * @type Boolean 1275 * @final 1276 */ 1277 iOS: iDevice, 1278 1279 /** 1280 * Constant that is true if the browser supports editing. 1281 * 1282 * @property contentEditable 1283 * @type Boolean 1284 * @final 1285 */ 1286 contentEditable: contentEditable, 1287 1288 /** 1289 * Transparent image data url. 1290 * 1291 * @property transparentSrc 1292 * @type Boolean 1293 * @final 1294 */ 1295 transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", 1296 1297 /** 1298 * Returns true/false if the browser can or can't place the caret after a inline block like an image. 1299 * 1300 * @property noCaretAfter 1301 * @type Boolean 1302 * @final 1303 */ 1304 caretAfter: ie != 8, 1305 1306 /** 1307 * Constant that is true if the browser supports native DOM Ranges. IE 9+. 1308 * 1309 * @property range 1310 * @type Boolean 1311 */ 1312 range: window.getSelection && "Range" in window, 1313 1314 /** 1315 * Returns the IE document mode for non IE browsers this will fake IE 10. 1316 * 1317 * @property documentMode 1318 * @type Number 1319 */ 1320 documentMode: ie ? (document.documentMode || 7) : 10 1321 }; 1322 }); 1323 1324 // Included from: js/tinymce/classes/dom/DomQuery.js 1325 1326 /** 1327 * DomQuery.js 1328 * 1329 * Copyright, Moxiecode Systems AB 1330 * Released under LGPL License. 1331 * 1332 * License: http://www.tinymce.com/license 1333 * Contributing: http://www.tinymce.com/contributing 1334 */ 1335 1336 /** 1337 * This class mimics most of the jQuery API: 1338 * 1339 * This is whats currently implemented: 1340 * - Utility functions 1341 * - DOM traversial 1342 * - DOM manipulation 1343 * - Event binding 1344 * 1345 * This is not currently implemented: 1346 * - Dimension 1347 * - Ajax 1348 * - Animation 1349 * - Advanced chaining 1350 * 1351 * @example 1352 * var $ = tinymce.dom.DomQuery; 1353 * $('p').attr('attr', 'value').addClass('class'); 1354 * 1355 * @class tinymce.dom.DomQuery 1356 */ 1357 define("tinymce/dom/DomQuery", [ 1358 "tinymce/dom/EventUtils", 1359 "tinymce/dom/Sizzle", 1360 "tinymce/util/Tools", 1361 "tinymce/Env" 1362 ], function(EventUtils, Sizzle, Tools, Env) { 1363 var doc = document, push = Array.prototype.push, slice = Array.prototype.slice; 1364 var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/; 1365 var Event = EventUtils.Event, undef; 1366 1367 function isDefined(obj) { 1368 return typeof obj !== 'undefined'; 1369 } 1370 1371 function isString(obj) { 1372 return typeof obj === 'string'; 1373 } 1374 1375 function createFragment(html, fragDoc) { 1376 var frag, node, container; 1377 1378 fragDoc = fragDoc || doc; 1379 container = fragDoc.createElement('div'); 1380 frag = fragDoc.createDocumentFragment(); 1381 container.innerHTML = html; 1382 1383 while ((node = container.firstChild)) { 1384 frag.appendChild(node); 1385 } 1386 1387 return frag; 1388 } 1389 1390 function domManipulate(targetNodes, sourceItem, callback, reverse) { 1391 var i; 1392 1393 if (isString(sourceItem)) { 1394 sourceItem = createFragment(sourceItem, getElementDocument(targetNodes[0])); 1395 } else if (sourceItem.length && !sourceItem.nodeType) { 1396 sourceItem = DomQuery.makeArray(sourceItem); 1397 1398 if (reverse) { 1399 for (i = sourceItem.length - 1; i >= 0; i--) { 1400 domManipulate(targetNodes, sourceItem[i], callback, reverse); 1401 } 1402 } else { 1403 for (i = 0; i < sourceItem.length; i++) { 1404 domManipulate(targetNodes, sourceItem[i], callback, reverse); 1405 } 1406 } 1407 1408 return targetNodes; 1409 } 1410 1411 if (sourceItem.nodeType) { 1412 i = targetNodes.length; 1413 while (i--) { 1414 callback.call(targetNodes[i], sourceItem); 1415 } 1416 } 1417 1418 return targetNodes; 1419 } 1420 1421 function hasClass(node, className) { 1422 return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1; 1423 } 1424 1425 function wrap(elements, wrapper, all) { 1426 var lastParent, newWrapper; 1427 1428 wrapper = DomQuery(wrapper)[0]; 1429 1430 elements.each(function() { 1431 var self = this; 1432 1433 if (!all || lastParent != self.parentNode) { 1434 lastParent = self.parentNode; 1435 newWrapper = wrapper.cloneNode(false); 1436 self.parentNode.insertBefore(newWrapper, self); 1437 newWrapper.appendChild(self); 1438 } else { 1439 newWrapper.appendChild(self); 1440 } 1441 }); 1442 1443 return elements; 1444 } 1445 1446 var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' '); 1447 var booleanMap = Tools.makeMap('checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected', ' '); 1448 var propFix = { 1449 'for': 'htmlFor', 1450 'class': 'className', 1451 'readonly': 'readOnly' 1452 }; 1453 var cssFix = { 1454 'float': 'cssFloat' 1455 }; 1456 1457 var attrHooks = {}, cssHooks = {}; 1458 1459 function DomQuery(selector, context) { 1460 /*eslint new-cap:0 */ 1461 return new DomQuery.fn.init(selector, context); 1462 } 1463 1464 function inArray(item, array) { 1465 var i; 1466 1467 if (array.indexOf) { 1468 return array.indexOf(item); 1469 } 1470 1471 i = array.length; 1472 while (i--) { 1473 if (array[i] === item) { 1474 return i; 1475 } 1476 } 1477 1478 return -1; 1479 } 1480 1481 var whiteSpaceRegExp = /^\s*|\s*$/g; 1482 1483 function trim(str) { 1484 return (str === null || str === undef) ? '' : ("" + str).replace(whiteSpaceRegExp, ''); 1485 } 1486 1487 function each(obj, callback) { 1488 var length, key, i, undef, value; 1489 1490 if (obj) { 1491 length = obj.length; 1492 1493 if (length === undef) { 1494 // Loop object items 1495 for (key in obj) { 1496 if (obj.hasOwnProperty(key)) { 1497 value = obj[key]; 1498 if (callback.call(value, key, value) === false) { 1499 break; 1500 } 1501 } 1502 } 1503 } else { 1504 // Loop array items 1505 for (i = 0; i < length; i++) { 1506 value = obj[i]; 1507 if (callback.call(value, i, value) === false) { 1508 break; 1509 } 1510 } 1511 } 1512 } 1513 1514 return obj; 1515 } 1516 1517 function grep(array, callback) { 1518 var out = []; 1519 1520 each(array, function(i, item) { 1521 if (callback(item, i)) { 1522 out.push(item); 1523 } 1524 }); 1525 1526 return out; 1527 } 1528 1529 function getElementDocument(element) { 1530 if (!element) { 1531 return doc; 1532 } 1533 1534 if (element.nodeType == 9) { 1535 return element; 1536 } 1537 1538 return element.ownerDocument; 1539 } 1540 1541 DomQuery.fn = DomQuery.prototype = { 1542 constructor: DomQuery, 1543 1544 /** 1545 * Selector for the current set. 1546 * 1547 * @property selector 1548 * @type String 1549 */ 1550 selector: "", 1551 1552 /** 1553 * Context used to create the set. 1554 * 1555 * @property context 1556 * @type Element 1557 */ 1558 context: null, 1559 1560 /** 1561 * Number of items in the current set. 1562 * 1563 * @property length 1564 * @type Number 1565 */ 1566 length: 0, 1567 1568 /** 1569 * Constructs a new DomQuery instance with the specified selector or context. 1570 * 1571 * @constructor 1572 * @method init 1573 * @param {String/Array/DomQuery} selector Optional CSS selector/Array or array like object or HTML string. 1574 * @param {Document/Element} context Optional context to search in. 1575 */ 1576 init: function(selector, context) { 1577 var self = this, match, node; 1578 1579 if (!selector) { 1580 return self; 1581 } 1582 1583 if (selector.nodeType) { 1584 self.context = self[0] = selector; 1585 self.length = 1; 1586 1587 return self; 1588 } 1589 1590 if (context && context.nodeType) { 1591 self.context = context; 1592 } else { 1593 if (context) { 1594 return DomQuery(selector).attr(context); 1595 } else { 1596 self.context = context = document; 1597 } 1598 } 1599 1600 if (isString(selector)) { 1601 self.selector = selector; 1602 1603 if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) { 1604 match = [null, selector, null]; 1605 } else { 1606 match = rquickExpr.exec(selector); 1607 } 1608 1609 if (match) { 1610 if (match[1]) { 1611 node = createFragment(selector, getElementDocument(context)).firstChild; 1612 1613 while (node) { 1614 push.call(self, node); 1615 node = node.nextSibling; 1616 } 1617 } else { 1618 node = getElementDocument(context).getElementById(match[2]); 1619 1620 if (!node) { 1621 return self; 1622 } 1623 1624 if (node.id !== match[2]) { 1625 return self.find(selector); 1626 } 1627 1628 self.length = 1; 1629 self[0] = node; 1630 } 1631 } else { 1632 return DomQuery(context).find(selector); 1633 } 1634 } else { 1635 this.add(selector, false); 1636 } 1637 1638 return self; 1639 }, 1640 1641 /** 1642 * Converts the current set to an array. 1643 * 1644 * @method toArray 1645 * @param {Array} Array of all nodes in set. 1646 */ 1647 toArray: function() { 1648 return Tools.toArray(this); 1649 }, 1650 1651 /** 1652 * Adds new nodes to the set. 1653 * 1654 * @method add 1655 * @param {Array/tinymce.dom.DomQuery} items Array of all nodes to add to set. 1656 * @return {tinymce.dom.DomQuery} New instance with nodes added. 1657 */ 1658 add: function(items, sort) { 1659 var self = this, nodes, i; 1660 1661 if (isString(items)) { 1662 return self.add(DomQuery(items)); 1663 } 1664 1665 if (items.nodeType) { 1666 return self.add([items]); 1667 } 1668 1669 if (sort !== false) { 1670 nodes = DomQuery.unique(self.toArray().concat(DomQuery.makeArray(items))); 1671 self.length = nodes.length; 1672 for (i = 0; i < nodes.length; i++) { 1673 self[i] = nodes[i]; 1674 } 1675 } else { 1676 push.apply(self, DomQuery.makeArray(items)); 1677 } 1678 1679 return self; 1680 }, 1681 1682 /** 1683 * Sets/gets attributes on the elements in the current set. 1684 * 1685 * @method attr 1686 * @param {String/Object} name Name of attribute to get or an object with attributes to set. 1687 * @param {String} value Optional value to set. 1688 * @return {tinymce.dom.DomQuery/String} Current set or the specified attribute when only the name is specified. 1689 */ 1690 attr: function(name, value) { 1691 var self = this, hook; 1692 1693 if (typeof name === "object") { 1694 each(name, function(name, value) { 1695 self.attr(name, value); 1696 }); 1697 } else if (isDefined(value)) { 1698 this.each(function() { 1699 var hook; 1700 1701 if (this.nodeType === 1) { 1702 hook = attrHooks[name]; 1703 if (hook && hook.set) { 1704 hook.set(this, value); 1705 return; 1706 } 1707 1708 if (value === null) { 1709 this.removeAttribute(name, 2); 1710 } else { 1711 this.setAttribute(name, value, 2); 1712 } 1713 } 1714 }); 1715 } else { 1716 if (self[0] && self[0].nodeType === 1) { 1717 hook = attrHooks[name]; 1718 if (hook && hook.get) { 1719 return hook.get(self[0], name); 1720 } 1721 1722 if (booleanMap[name]) { 1723 return self.prop(name) ? name : undef; 1724 } 1725 1726 value = self[0].getAttribute(name, 2); 1727 1728 if (value === null) { 1729 value = undef; 1730 } 1731 } 1732 1733 return value; 1734 } 1735 1736 return self; 1737 }, 1738 1739 /** 1740 * Removes attributse on the elements in the current set. 1741 * 1742 * @method removeAttr 1743 * @param {String/Object} name Name of attribute to remove. 1744 * @return {tinymce.dom.DomQuery/String} Current set. 1745 */ 1746 removeAttr: function(name) { 1747 return this.attr(name, null); 1748 }, 1749 1750 /** 1751 * Sets/gets properties on the elements in the current set. 1752 * 1753 * @method attr 1754 * @param {String/Object} name Name of property to get or an object with properties to set. 1755 * @param {String} value Optional value to set. 1756 * @return {tinymce.dom.DomQuery/String} Current set or the specified property when only the name is specified. 1757 */ 1758 prop: function(name, value) { 1759 var self = this; 1760 1761 name = propFix[name] || name; 1762 1763 if (typeof name === "object") { 1764 each(name, function(name, value) { 1765 self.prop(name, value); 1766 }); 1767 } else if (isDefined(value)) { 1768 this.each(function() { 1769 if (this.nodeType == 1) { 1770 this[name] = value; 1771 } 1772 }); 1773 } else { 1774 if (self[0] && self[0].nodeType && name in self[0]) { 1775 return self[0][name]; 1776 } 1777 1778 return value; 1779 } 1780 1781 return self; 1782 }, 1783 1784 /** 1785 * Sets/gets styles on the elements in the current set. 1786 * 1787 * @method css 1788 * @param {String/Object} name Name of style to get or an object with styles to set. 1789 * @param {String} value Optional value to set. 1790 * @return {tinymce.dom.DomQuery/String} Current set or the specified style when only the name is specified. 1791 */ 1792 css: function(name, value) { 1793 var self = this, elm, hook; 1794 1795 function camel(name) { 1796 return name.replace(/-(\D)/g, function(a, b) { 1797 return b.toUpperCase(); 1798 }); 1799 } 1800 1801 function dashed(name) { 1802 return name.replace(/[A-Z]/g, function(a) { 1803 return '-' + a; 1804 }); 1805 } 1806 1807 if (typeof name === "object") { 1808 each(name, function(name, value) { 1809 self.css(name, value); 1810 }); 1811 } else { 1812 if (isDefined(value)) { 1813 name = camel(name); 1814 1815 // Default px suffix on these 1816 if (typeof(value) === 'number' && !numericCssMap[name]) { 1817 value += 'px'; 1818 } 1819 1820 self.each(function() { 1821 var style = this.style; 1822 1823 hook = cssHooks[name]; 1824 if (hook && hook.set) { 1825 hook.set(this, value); 1826 return; 1827 } 1828 1829 try { 1830 this.style[cssFix[name] || name] = value; 1831 } catch (ex) { 1832 // Ignore 1833 } 1834 1835 if (value === null || value === '') { 1836 if (style.removeProperty) { 1837 style.removeProperty(dashed(name)); 1838 } else { 1839 style.removeAttribute(name); 1840 } 1841 } 1842 }); 1843 } else { 1844 elm = self[0]; 1845 1846 hook = cssHooks[name]; 1847 if (hook && hook.get) { 1848 return hook.get(elm); 1849 } 1850 1851 if (elm.ownerDocument.defaultView) { 1852 try { 1853 return elm.ownerDocument.defaultView.getComputedStyle(elm, null).getPropertyValue(dashed(name)); 1854 } catch (ex) { 1855 return undef; 1856 } 1857 } else if (elm.currentStyle) { 1858 return elm.currentStyle[camel(name)]; 1859 } 1860 } 1861 } 1862 1863 return self; 1864 }, 1865 1866 /** 1867 * Removes all nodes in set from the document. 1868 * 1869 * @method remove 1870 * @return {tinymce.dom.DomQuery} Current set with the removed nodes. 1871 */ 1872 remove: function() { 1873 var self = this, node, i = this.length; 1874 1875 while (i--) { 1876 node = self[i]; 1877 Event.clean(node); 1878 1879 if (node.parentNode) { 1880 node.parentNode.removeChild(node); 1881 } 1882 } 1883 1884 return this; 1885 }, 1886 1887 /** 1888 * Empties all elements in set. 1889 * 1890 * @method empty 1891 * @return {tinymce.dom.DomQuery} Current set with the empty nodes. 1892 */ 1893 empty: function() { 1894 var self = this, node, i = this.length; 1895 1896 while (i--) { 1897 node = self[i]; 1898 while (node.firstChild) { 1899 node.removeChild(node.firstChild); 1900 } 1901 } 1902 1903 return this; 1904 }, 1905 1906 /** 1907 * Sets or gets the HTML of the current set or first set node. 1908 * 1909 * @method html 1910 * @param {String} value Optional innerHTML value to set on each element. 1911 * @return {tinymce.dom.DomQuery/String} Current set or the innerHTML of the first element. 1912 */ 1913 html: function(value) { 1914 var self = this, i; 1915 1916 if (isDefined(value)) { 1917 i = self.length; 1918 1919 try { 1920 while (i--) { 1921 self[i].innerHTML = value; 1922 } 1923 } catch (ex) { 1924 // Workaround for "Unknown runtime error" when DIV is added to P on IE 1925 DomQuery(self[i]).empty().append(value); 1926 } 1927 1928 return self; 1929 } 1930 1931 return self[0] ? self[0].innerHTML : ''; 1932 }, 1933 1934 /** 1935 * Sets or gets the text of the current set or first set node. 1936 * 1937 * @method text 1938 * @param {String} value Optional innerText value to set on each element. 1939 * @return {tinymce.dom.DomQuery/String} Current set or the innerText of the first element. 1940 */ 1941 text: function(value) { 1942 var self = this, i; 1943 1944 if (isDefined(value)) { 1945 i = self.length; 1946 while (i--) { 1947 if ("innerText" in self[i]) { 1948 self[i].innerText = value; 1949 } else { 1950 self[0].textContent = value; 1951 } 1952 } 1953 1954 return self; 1955 } 1956 1957 return self[0] ? (self[0].innerText || self[0].textContent) : ''; 1958 }, 1959 1960 /** 1961 * Appends the specified node/html or node set to the current set nodes. 1962 * 1963 * @method append 1964 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to append to each element in set. 1965 * @return {tinymce.dom.DomQuery} Current set. 1966 */ 1967 append: function() { 1968 return domManipulate(this, arguments, function(node) { 1969 if (this.nodeType === 1) { 1970 this.appendChild(node); 1971 } 1972 }); 1973 }, 1974 1975 /** 1976 * Prepends the specified node/html or node set to the current set nodes. 1977 * 1978 * @method prepend 1979 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to prepend to each element in set. 1980 * @return {tinymce.dom.DomQuery} Current set. 1981 */ 1982 prepend: function() { 1983 return domManipulate(this, arguments, function(node) { 1984 if (this.nodeType === 1) { 1985 this.insertBefore(node, this.firstChild); 1986 } 1987 }, true); 1988 }, 1989 1990 /** 1991 * Adds the specified elements before current set nodes. 1992 * 1993 * @method before 1994 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add before to each element in set. 1995 * @return {tinymce.dom.DomQuery} Current set. 1996 */ 1997 before: function() { 1998 var self = this; 1999 2000 if (self[0] && self[0].parentNode) { 2001 return domManipulate(self, arguments, function(node) { 2002 this.parentNode.insertBefore(node, this); 2003 }); 2004 } 2005 2006 return self; 2007 }, 2008 2009 /** 2010 * Adds the specified elements after current set nodes. 2011 * 2012 * @method after 2013 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add after to each element in set. 2014 * @return {tinymce.dom.DomQuery} Current set. 2015 */ 2016 after: function() { 2017 var self = this; 2018 2019 if (self[0] && self[0].parentNode) { 2020 return domManipulate(self, arguments, function(node) { 2021 this.parentNode.insertBefore(node, this.nextSibling); 2022 }, true); 2023 } 2024 2025 return self; 2026 }, 2027 2028 /** 2029 * Appends the specified set nodes to the specified selector/instance. 2030 * 2031 * @method appendTo 2032 * @param {String/Element/Array/tinymce.dom.DomQuery} val Item to append the current set to. 2033 * @return {tinymce.dom.DomQuery} Current set with the appended nodes. 2034 */ 2035 appendTo: function(val) { 2036 DomQuery(val).append(this); 2037 2038 return this; 2039 }, 2040 2041 /** 2042 * Prepends the specified set nodes to the specified selector/instance. 2043 * 2044 * @method prependTo 2045 * @param {String/Element/Array/tinymce.dom.DomQuery} val Item to prepend the current set to. 2046 * @return {tinymce.dom.DomQuery} Current set with the prepended nodes. 2047 */ 2048 prependTo: function(val) { 2049 DomQuery(val).prepend(this); 2050 2051 return this; 2052 }, 2053 2054 /** 2055 * Replaces the nodes in set with the specified content. 2056 * 2057 * @method replaceWith 2058 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to replace nodes with. 2059 * @return {tinymce.dom.DomQuery} Set with replaced nodes. 2060 */ 2061 replaceWith: function(content) { 2062 return this.before(content).remove(); 2063 }, 2064 2065 /** 2066 * Wraps all elements in set with the specified wrapper. 2067 * 2068 * @method wrap 2069 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with. 2070 * @return {tinymce.dom.DomQuery} Set with wrapped nodes. 2071 */ 2072 wrap: function(wrapper) { 2073 return wrap(this, wrapper); 2074 }, 2075 2076 /** 2077 * Wraps all nodes in set with the specified wrapper. If the nodes are siblings all of them 2078 * will be wrapped in the same wrapper. 2079 * 2080 * @method wrapAll 2081 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with. 2082 * @return {tinymce.dom.DomQuery} Set with wrapped nodes. 2083 */ 2084 wrapAll: function(wrapper) { 2085 return wrap(this, wrapper, true); 2086 }, 2087 2088 /** 2089 * Wraps all elements inner contents in set with the specified wrapper. 2090 * 2091 * @method wrapInner 2092 * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with. 2093 * @return {tinymce.dom.DomQuery} Set with wrapped nodes. 2094 */ 2095 wrapInner: function(wrapper) { 2096 this.each(function() { 2097 DomQuery(this).contents().wrapAll(wrapper); 2098 }); 2099 2100 return this; 2101 }, 2102 2103 /** 2104 * Unwraps all elements by removing the parent element of each item in set. 2105 * 2106 * @method unwrap 2107 * @return {tinymce.dom.DomQuery} Set with unwrapped nodes. 2108 */ 2109 unwrap: function() { 2110 return this.parent().each(function() { 2111 DomQuery(this).replaceWith(this.childNodes); 2112 }); 2113 }, 2114 2115 /** 2116 * Clones all nodes in set. 2117 * 2118 * @method clone 2119 * @return {tinymce.dom.DomQuery} Set with cloned nodes. 2120 */ 2121 clone: function() { 2122 var result = []; 2123 2124 this.each(function() { 2125 result.push(this.cloneNode(true)); 2126 }); 2127 2128 return DomQuery(result); 2129 }, 2130 2131 /** 2132 * Adds the specified class name to the current set elements. 2133 * 2134 * @method addClass 2135 * @param {String} className Class name to add. 2136 * @return {tinymce.dom.DomQuery} Current set. 2137 */ 2138 addClass: function(className) { 2139 return this.toggleClass(className, true); 2140 }, 2141 2142 /** 2143 * Removes the specified class name to the current set elements. 2144 * 2145 * @method removeClass 2146 * @param {String} className Class name to remove. 2147 * @return {tinymce.dom.DomQuery} Current set. 2148 */ 2149 removeClass: function(className) { 2150 return this.toggleClass(className, false); 2151 }, 2152 2153 /** 2154 * Toggles the specified class name on the current set elements. 2155 * 2156 * @method toggleClass 2157 * @param {String} className Class name to add/remove. 2158 * @param {Boolean} state Optional state to toggle on/off. 2159 * @return {tinymce.dom.DomQuery} Current set. 2160 */ 2161 toggleClass: function(className, state) { 2162 var self = this; 2163 2164 // Functions are not supported 2165 if (typeof className != 'string') { 2166 return self; 2167 } 2168 2169 if (className.indexOf(' ') !== -1) { 2170 each(className.split(' '), function() { 2171 self.toggleClass(this, state); 2172 }); 2173 } else { 2174 self.each(function(index, node) { 2175 var existingClassName, classState; 2176 2177 classState = hasClass(node, className); 2178 if (classState !== state) { 2179 existingClassName = node.className; 2180 2181 if (classState) { 2182 node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' ')); 2183 } else { 2184 node.className += existingClassName ? ' ' + className : className; 2185 } 2186 } 2187 }); 2188 } 2189 2190 return self; 2191 }, 2192 2193 /** 2194 * Returns true/false if the first item in set has the specified class. 2195 * 2196 * @method hasClass 2197 * @param {String} className Class name to check for. 2198 * @return {Boolean} True/false if the set has the specified class. 2199 */ 2200 hasClass: function(className) { 2201 return hasClass(this[0], className); 2202 }, 2203 2204 /** 2205 * Executes the callback function for each item DomQuery collection. If you return false in the 2206 * callback it will break the loop. 2207 * 2208 * @method each 2209 * @param {function} callback Callback function to execute for each item. 2210 * @return {tinymce.dom.DomQuery} Current set. 2211 */ 2212 each: function(callback) { 2213 return each(this, callback); 2214 }, 2215 2216 /** 2217 * Binds an event with callback function to the elements in set. 2218 * 2219 * @method on 2220 * @param {String} name Name of the event to bind. 2221 * @param {function} callback Callback function to execute when the event occurs. 2222 * @return {tinymce.dom.DomQuery} Current set. 2223 */ 2224 on: function(name, callback) { 2225 return this.each(function() { 2226 Event.bind(this, name, callback); 2227 }); 2228 }, 2229 2230 /** 2231 * Unbinds an event with callback function to the elements in set. 2232 * 2233 * @method off 2234 * @param {String} name Optional name of the event to bind. 2235 * @param {function} callback Optional callback function to execute when the event occurs. 2236 * @return {tinymce.dom.DomQuery} Current set. 2237 */ 2238 off: function(name, callback) { 2239 return this.each(function() { 2240 Event.unbind(this, name, callback); 2241 }); 2242 }, 2243 2244 /** 2245 * Triggers the specified event by name or event object. 2246 * 2247 * @method trigger 2248 * @param {String/Object} name Name of the event to trigger or event object. 2249 * @return {tinymce.dom.DomQuery} Current set. 2250 */ 2251 trigger: function(name) { 2252 return this.each(function() { 2253 if (typeof name == 'object') { 2254 Event.fire(this, name.type, name); 2255 } else { 2256 Event.fire(this, name); 2257 } 2258 }); 2259 }, 2260 2261 /** 2262 * Shows all elements in set. 2263 * 2264 * @method show 2265 * @return {tinymce.dom.DomQuery} Current set. 2266 */ 2267 show: function() { 2268 return this.css('display', ''); 2269 }, 2270 2271 /** 2272 * Hides all elements in set. 2273 * 2274 * @method hide 2275 * @return {tinymce.dom.DomQuery} Current set. 2276 */ 2277 hide: function() { 2278 return this.css('display', 'none'); 2279 }, 2280 2281 /** 2282 * Slices the current set. 2283 * 2284 * @method slice 2285 * @param {Number} start Start index to slice at. 2286 * @param {Number} end Optional ened index to end slice at. 2287 * @return {tinymce.dom.DomQuery} Sliced set. 2288 */ 2289 slice: function() { 2290 return new DomQuery(slice.apply(this, arguments)); 2291 }, 2292 2293 /** 2294 * Makes the set equal to the specified index. 2295 * 2296 * @method eq 2297 * @param {Number} index Index to set it equal to. 2298 * @return {tinymce.dom.DomQuery} Single item set. 2299 */ 2300 eq: function(index) { 2301 return index === -1 ? this.slice(index) : this.slice(index, +index + 1); 2302 }, 2303 2304 /** 2305 * Makes the set equal to first element in set. 2306 * 2307 * @method first 2308 * @return {tinymce.dom.DomQuery} Single item set. 2309 */ 2310 first: function() { 2311 return this.eq(0); 2312 }, 2313 2314 /** 2315 * Makes the set equal to last element in set. 2316 * 2317 * @method last 2318 * @return {tinymce.dom.DomQuery} Single item set. 2319 */ 2320 last: function() { 2321 return this.eq(-1); 2322 }, 2323 2324 /** 2325 * Finds elements by the specified selector for each element in set. 2326 * 2327 * @method find 2328 * @param {String} selector Selector to find elements by. 2329 * @return {tinymce.dom.DomQuery} Set with matches elements. 2330 */ 2331 find: function(selector) { 2332 var i, l, ret = []; 2333 2334 for (i = 0, l = this.length; i < l; i++) { 2335 DomQuery.find(selector, this[i], ret); 2336 } 2337 2338 return DomQuery(ret); 2339 }, 2340 2341 /** 2342 * Filters the current set with the specified selector. 2343 * 2344 * @method filter 2345 * @param {String/function} selector Selector to filter elements by. 2346 * @return {tinymce.dom.DomQuery} Set with filtered elements. 2347 */ 2348 filter: function(selector) { 2349 if (typeof selector == 'function') { 2350 return DomQuery(grep(this.toArray(), function(item, i) { 2351 return selector(i, item); 2352 })); 2353 } 2354 2355 return DomQuery(DomQuery.filter(selector, this.toArray())); 2356 }, 2357 2358 /** 2359 * Gets the current node or any partent matching the specified selector. 2360 * 2361 * @method closest 2362 * @param {String/Element/tinymce.dom.DomQuery} selector Selector or element to find. 2363 * @return {tinymce.dom.DomQuery} Set with closest elements. 2364 */ 2365 closest: function(selector) { 2366 var result = []; 2367 2368 if (selector instanceof DomQuery) { 2369 selector = selector[0]; 2370 } 2371 2372 this.each(function(i, node) { 2373 while (node) { 2374 if (typeof selector == 'string' && DomQuery(node).is(selector)) { 2375 result.push(node); 2376 break; 2377 } else if (node == selector) { 2378 result.push(node); 2379 break; 2380 } 2381 2382 node = node.parentNode; 2383 } 2384 }); 2385 2386 return DomQuery(result); 2387 }, 2388 2389 /** 2390 * Returns the offset of the first element in set or sets the top/left css properties of all elements in set. 2391 * 2392 * @method offset 2393 * @param {Object} offset Optional offset object to set on each item. 2394 * @return {Object/tinymce.dom.DomQuery} Returns the first element offset or the current set if you specified an offset. 2395 */ 2396 offset: function(offset) { 2397 var elm, doc, docElm; 2398 var x = 0, y = 0, pos; 2399 2400 if (!offset) { 2401 elm = this[0]; 2402 2403 if (elm) { 2404 doc = elm.ownerDocument; 2405 docElm = doc.documentElement; 2406 2407 if (elm.getBoundingClientRect) { 2408 pos = elm.getBoundingClientRect(); 2409 x = pos.left + (docElm.scrollLeft || doc.body.scrollLeft) - docElm.clientLeft; 2410 y = pos.top + (docElm.scrollTop || doc.body.scrollTop) - docElm.clientTop; 2411 } 2412 } 2413 2414 return { 2415 left: x, 2416 top: y 2417 }; 2418 } 2419 2420 return this.css(offset); 2421 }, 2422 2423 push: push, 2424 sort: [].sort, 2425 splice: [].splice 2426 }; 2427 2428 // Static members 2429 Tools.extend(DomQuery, { 2430 /** 2431 * Extends the specified object with one or more objects. 2432 * 2433 * @static 2434 * @method extend 2435 * @param {Object} target Target object to extend with new items. 2436 * @param {Object..} object Object to extend the target with. 2437 * @return {Object} Extended input object. 2438 */ 2439 extend: Tools.extend, 2440 2441 /** 2442 * Creates an array out of an array like object. 2443 * 2444 * @static 2445 * @method makeArray 2446 * @param {Object} object Object to convert to array. 2447 * @return {Arrau} Array produced from object. 2448 */ 2449 makeArray: Tools.toArray, 2450 2451 /** 2452 * Returns the index of the specified item inside the array. 2453 * 2454 * @static 2455 * @method inArray 2456 * @param {Object} item Item to look for. 2457 * @param {Array} array Array to look for item in. 2458 * @return {Number} Index of the item or -1. 2459 */ 2460 inArray: inArray, 2461 2462 /** 2463 * Returns true/false if the specified object is an array or not. 2464 * 2465 * @static 2466 * @method isArray 2467 * @param {Object} array Object to check if it's an array or not. 2468 * @return {Boolean} True/false if the object is an array. 2469 */ 2470 isArray: Tools.isArray, 2471 2472 /** 2473 * Executes the callback function for each item in array/object. If you return false in the 2474 * callback it will break the loop. 2475 * 2476 * @static 2477 * @method each 2478 * @param {Object} obj Object to iterate. 2479 * @param {function} callback Callback function to execute for each item. 2480 */ 2481 each: each, 2482 2483 /** 2484 * Removes whitespace from the beginning and end of a string. 2485 * 2486 * @static 2487 * @method trim 2488 * @param {String} str String to remove whitespace from. 2489 * @return {String} New string with removed whitespace. 2490 */ 2491 trim: trim, 2492 2493 /** 2494 * Filters out items from the input array by calling the specified function for each item. 2495 * If the function returns false the item will be excluded if it returns true it will be included. 2496 * 2497 * @static 2498 * @method grep 2499 * @param {Array} array Array of items to loop though. 2500 * @param {function} callback Function to call for each item. Include/exclude depends on it's return value. 2501 * @return {Array} New array with values imported and filtered based in input. 2502 * @example 2503 * // Filter out some items, this will return an array with 4 and 5 2504 * var items = DomQuery.grep([1, 2, 3, 4, 5], function(v) {return v > 3;}); 2505 */ 2506 grep: grep, 2507 2508 // Sizzle 2509 find: Sizzle, 2510 expr: Sizzle.selectors, 2511 unique: Sizzle.uniqueSort, 2512 text: Sizzle.getText, 2513 contains: Sizzle.contains, 2514 filter: function(expr, elems, not) { 2515 var i = elems.length; 2516 2517 if (not) { 2518 expr = ":not(" + expr + ")"; 2519 } 2520 2521 while (i--) { 2522 if (elems[i].nodeType != 1) { 2523 elems.splice(i, 1); 2524 } 2525 } 2526 2527 if (elems.length === 1) { 2528 elems = DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : []; 2529 } else { 2530 elems = DomQuery.find.matches(expr, elems); 2531 } 2532 2533 return elems; 2534 } 2535 }); 2536 2537 function dir(el, prop, until) { 2538 var matched = [], cur = el[prop]; 2539 2540 if (typeof until != 'string' && until instanceof DomQuery) { 2541 until = until[0]; 2542 } 2543 2544 while (cur && cur.nodeType !== 9) { 2545 if (until !== undefined) { 2546 if (cur === until) { 2547 break; 2548 } 2549 2550 if (typeof until == 'string' && DomQuery(cur).is(until)) { 2551 break; 2552 } 2553 } 2554 2555 if (cur.nodeType === 1) { 2556 matched.push(cur); 2557 } 2558 2559 cur = cur[prop]; 2560 } 2561 2562 return matched; 2563 } 2564 2565 function sibling(node, siblingName, nodeType, until) { 2566 var result = []; 2567 2568 if (until instanceof DomQuery) { 2569 until = until[0]; 2570 } 2571 2572 for (; node; node = node[siblingName]) { 2573 if (nodeType && node.nodeType !== nodeType) { 2574 continue; 2575 } 2576 2577 if (until !== undefined) { 2578 if (node === until) { 2579 break; 2580 } 2581 2582 if (typeof until == 'string' && DomQuery(node).is(until)) { 2583 break; 2584 } 2585 } 2586 2587 result.push(node); 2588 } 2589 2590 return result; 2591 } 2592 2593 function firstSibling(node, siblingName, nodeType) { 2594 for (node = node[siblingName]; node; node = node[siblingName]) { 2595 if (node.nodeType == nodeType) { 2596 return node; 2597 } 2598 } 2599 2600 return null; 2601 } 2602 2603 each({ 2604 /** 2605 * Returns a new collection with the parent of each item in current collection matching the optional selector. 2606 * 2607 * @method parent 2608 * @param {String} selector Selector to match parents agains. 2609 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents. 2610 */ 2611 parent: function(node) { 2612 var parent = node.parentNode; 2613 2614 return parent && parent.nodeType !== 11 ? parent : null; 2615 }, 2616 2617 /** 2618 * Returns a new collection with the all the parents of each item in current collection matching the optional selector. 2619 * 2620 * @method parents 2621 * @param {String} selector Selector to match parents agains. 2622 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents. 2623 */ 2624 parents: function(node) { 2625 return dir(node, "parentNode"); 2626 }, 2627 2628 /** 2629 * Returns a new collection with next sibling of each item in current collection matching the optional selector. 2630 * 2631 * @method next 2632 * @param {String} selector Selector to match the next element against. 2633 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements. 2634 */ 2635 next: function(node) { 2636 return firstSibling(node, 'nextSibling', 1); 2637 }, 2638 2639 /** 2640 * Returns a new collection with previous sibling of each item in current collection matching the optional selector. 2641 * 2642 * @method prev 2643 * @param {String} selector Selector to match the previous element against. 2644 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements. 2645 */ 2646 prev: function(node) { 2647 return firstSibling(node, 'previousSibling', 1); 2648 }, 2649 2650 /** 2651 * Returns all child elements matching the optional selector. 2652 * 2653 * @method children 2654 * @param {String} selector Selector to match the elements against. 2655 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements. 2656 */ 2657 children: function(node) { 2658 return sibling(node.firstChild, 'nextSibling', 1); 2659 }, 2660 2661 /** 2662 * Returns all child nodes matching the optional selector. 2663 * 2664 * @method contents 2665 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements. 2666 */ 2667 contents: function(node) { 2668 return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes); 2669 } 2670 }, function(name, fn) { 2671 DomQuery.fn[name] = function(selector) { 2672 var self = this, result = []; 2673 2674 self.each(function() { 2675 var nodes = fn.call(result, this, selector, result); 2676 2677 if (nodes) { 2678 if (DomQuery.isArray(nodes)) { 2679 result.push.apply(result, nodes); 2680 } else { 2681 result.push(nodes); 2682 } 2683 } 2684 }); 2685 2686 // If traversing on multiple elements we might get the same elements twice 2687 if (this.length > 1) { 2688 result = DomQuery.unique(result); 2689 2690 if (name.indexOf('parents') === 0) { 2691 result = result.reverse(); 2692 } 2693 } 2694 2695 result = DomQuery(result); 2696 2697 if (selector) { 2698 return result.filter(selector); 2699 } 2700 2701 return result; 2702 }; 2703 }); 2704 2705 each({ 2706 /** 2707 * Returns a new collection with the all the parents until the matching selector/element 2708 * of each item in current collection matching the optional selector. 2709 * 2710 * @method parentsUntil 2711 * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element. 2712 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents. 2713 */ 2714 parentsUntil: function(node, until) { 2715 return dir(node, "parentNode", until); 2716 }, 2717 2718 /** 2719 * Returns a new collection with all next siblings of each item in current collection matching the optional selector. 2720 * 2721 * @method nextUntil 2722 * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element. 2723 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements. 2724 */ 2725 nextUntil: function(node, until) { 2726 return sibling(node, 'nextSibling', 1, until).slice(1); 2727 }, 2728 2729 /** 2730 * Returns a new collection with all previous siblings of each item in current collection matching the optional selector. 2731 * 2732 * @method prevUntil 2733 * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element. 2734 * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements. 2735 */ 2736 prevUntil: function(node, until) { 2737 return sibling(node, 'previousSibling', 1, until).slice(1); 2738 } 2739 }, function(name, fn) { 2740 DomQuery.fn[name] = function(selector, filter) { 2741 var self = this, result = []; 2742 2743 self.each(function() { 2744 var nodes = fn.call(result, this, selector, result); 2745 2746 if (nodes) { 2747 if (DomQuery.isArray(nodes)) { 2748 result.push.apply(result, nodes); 2749 } else { 2750 result.push(nodes); 2751 } 2752 } 2753 }); 2754 2755 // If traversing on multiple elements we might get the same elements twice 2756 if (this.length > 1) { 2757 result = DomQuery.unique(result); 2758 2759 if (name.indexOf('parents') === 0 || name === 'prevUntil') { 2760 result = result.reverse(); 2761 } 2762 } 2763 2764 result = DomQuery(result); 2765 2766 if (filter) { 2767 return result.filter(filter); 2768 } 2769 2770 return result; 2771 }; 2772 }); 2773 2774 /** 2775 * Returns true/false if the current set items matches the selector. 2776 * 2777 * @method is 2778 * @param {String} selector Selector to match the elements against. 2779 * @return {Boolean} True/false if the current set matches the selector. 2780 */ 2781 DomQuery.fn.is = function(selector) { 2782 return !!selector && this.filter(selector).length > 0; 2783 }; 2784 2785 DomQuery.fn.init.prototype = DomQuery.fn; 2786 2787 DomQuery.overrideDefaults = function(callback) { 2788 var defaults; 2789 2790 function sub(selector, context) { 2791 defaults = defaults || callback(); 2792 2793 if (arguments.length === 0) { 2794 selector = defaults.element; 2795 } 2796 2797 if (!context) { 2798 context = defaults.context; 2799 } 2800 2801 return new sub.fn.init(selector, context); 2802 } 2803 2804 DomQuery.extend(sub, this); 2805 2806 return sub; 2807 }; 2808 2809 function appendHooks(targetHooks, prop, hooks) { 2810 each(hooks, function(name, func) { 2811 targetHooks[name] = targetHooks[name] || {}; 2812 targetHooks[name][prop] = func; 2813 }); 2814 } 2815 2816 if (Env.ie && Env.ie < 8) { 2817 appendHooks(attrHooks, 'get', { 2818 maxlength: function(elm) { 2819 var value = elm.maxLength; 2820 2821 if (value === 0x7fffffff) { 2822 return undef; 2823 } 2824 2825 return value; 2826 }, 2827 2828 size: function(elm) { 2829 var value = elm.size; 2830 2831 if (value === 20) { 2832 return undef; 2833 } 2834 2835 return value; 2836 }, 2837 2838 'class': function(elm) { 2839 return elm.className; 2840 }, 2841 2842 style: function(elm) { 2843 var value = elm.style.cssText; 2844 2845 if (value.length === 0) { 2846 return undef; 2847 } 2848 2849 return value; 2850 } 2851 }); 2852 2853 appendHooks(attrHooks, 'set', { 2854 'class': function(elm, value) { 2855 elm.className = value; 2856 }, 2857 2858 style: function(elm, value) { 2859 elm.style.cssText = value; 2860 } 2861 }); 2862 } 2863 2864 if (Env.ie && Env.ie < 9) { 2865 /*jshint sub:true */ 2866 /*eslint dot-notation: 0*/ 2867 cssFix['float'] = 'styleFloat'; 2868 2869 appendHooks(cssHooks, 'set', { 2870 opacity: function(elm, value) { 2871 var style = elm.style; 2872 2873 if (value === null || value === '') { 2874 style.removeAttribute('filter'); 2875 } else { 2876 style.zoom = 1; 2877 style.filter = 'alpha(opacity=' + (value * 100) + ')'; 2878 } 2879 } 2880 }); 2881 } 2882 2883 DomQuery.attrHooks = attrHooks; 2884 DomQuery.cssHooks = cssHooks; 2885 2886 return DomQuery; 2887 }); 2888 2889 // Included from: js/tinymce/classes/html/Styles.js 2890 2891 /** 2892 * Styles.js 2893 * 2894 * Copyright, Moxiecode Systems AB 2895 * Released under LGPL License. 2896 * 2897 * License: http://www.tinymce.com/license 2898 * Contributing: http://www.tinymce.com/contributing 2899 */ 2900 2901 /** 2902 * This class is used to parse CSS styles it also compresses styles to reduce the output size. 2903 * 2904 * @example 2905 * var Styles = new tinymce.html.Styles({ 2906 * url_converter: function(url) { 2907 * return url; 2908 * } 2909 * }); 2910 * 2911 * styles = Styles.parse('border: 1px solid red'); 2912 * styles.color = 'red'; 2913 * 2914 * console.log(new tinymce.html.StyleSerializer().serialize(styles)); 2915 * 2916 * @class tinymce.html.Styles 2917 * @version 3.4 2918 */ 2919 define("tinymce/html/Styles", [], function() { 2920 return function(settings, schema) { 2921 /*jshint maxlen:255 */ 2922 /*eslint max-len:0 */ 2923 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, 2924 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, 2925 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, 2926 trimRightRegExp = /\s+$/, 2927 undef, i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF'; 2928 2929 settings = settings || {}; 2930 2931 if (schema) { 2932 validStyles = schema.getValidStyles(); 2933 invalidStyles = schema.getInvalidStyles(); 2934 } 2935 2936 encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); 2937 for (i = 0; i < encodingItems.length; i++) { 2938 encodingLookup[encodingItems[i]] = invisibleChar + i; 2939 encodingLookup[invisibleChar + i] = encodingItems[i]; 2940 } 2941 2942 function toHex(match, r, g, b) { 2943 function hex(val) { 2944 val = parseInt(val, 10).toString(16); 2945 2946 return val.length > 1 ? val : '0' + val; // 0 -> 00 2947 } 2948 2949 return '#' + hex(r) + hex(g) + hex(b); 2950 } 2951 2952 return { 2953 /** 2954 * Parses the specified RGB color value and returns a hex version of that color. 2955 * 2956 * @method toHex 2957 * @param {String} color RGB string value like rgb(1,2,3) 2958 * @return {String} Hex version of that RGB value like #FF00FF. 2959 */ 2960 toHex: function(color) { 2961 return color.replace(rgbRegExp, toHex); 2962 }, 2963 2964 /** 2965 * Parses the specified style value into an object collection. This parser will also 2966 * merge and remove any redundant items that browsers might have added. It will also convert non hex 2967 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 2968 * 2969 * @method parse 2970 * @param {String} css Style value to parse for example: border:1px solid red;. 2971 * @return {Object} Object representation of that style like {border: '1px solid red'} 2972 */ 2973 parse: function(css) { 2974 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter; 2975 var urlConverterScope = settings.url_converter_scope || this; 2976 2977 function compress(prefix, suffix, noJoin) { 2978 var top, right, bottom, left; 2979 2980 top = styles[prefix + '-top' + suffix]; 2981 if (!top) { 2982 return; 2983 } 2984 2985 right = styles[prefix + '-right' + suffix]; 2986 if (!right) { 2987 return; 2988 } 2989 2990 bottom = styles[prefix + '-bottom' + suffix]; 2991 if (!bottom) { 2992 return; 2993 } 2994 2995 left = styles[prefix + '-left' + suffix]; 2996 if (!left) { 2997 return; 2998 } 2999 3000 var box = [top, right, bottom, left]; 3001 i = box.length - 1; 3002 while (i--) { 3003 if (box[i] !== box[i + 1]) { 3004 break; 3005 } 3006 } 3007 3008 if (i > -1 && noJoin) { 3009 return; 3010 } 3011 3012 styles[prefix + suffix] = i == -1 ? box[0] : box.join(' '); 3013 delete styles[prefix + '-top' + suffix]; 3014 delete styles[prefix + '-right' + suffix]; 3015 delete styles[prefix + '-bottom' + suffix]; 3016 delete styles[prefix + '-left' + suffix]; 3017 } 3018 3019 /** 3020 * Checks if the specific style can be compressed in other words if all border-width are equal. 3021 */ 3022 function canCompress(key) { 3023 var value = styles[key], i; 3024 3025 if (!value) { 3026 return; 3027 } 3028 3029 value = value.split(' '); 3030 i = value.length; 3031 while (i--) { 3032 if (value[i] !== value[0]) { 3033 return false; 3034 } 3035 } 3036 3037 styles[key] = value[0]; 3038 3039 return true; 3040 } 3041 3042 /** 3043 * Compresses multiple styles into one style. 3044 */ 3045 function compress2(target, a, b, c) { 3046 if (!canCompress(a)) { 3047 return; 3048 } 3049 3050 if (!canCompress(b)) { 3051 return; 3052 } 3053 3054 if (!canCompress(c)) { 3055 return; 3056 } 3057 3058 // Compress 3059 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 3060 delete styles[a]; 3061 delete styles[b]; 3062 delete styles[c]; 3063 } 3064 3065 // Encodes the specified string by replacing all \" \' ; : with _<num> 3066 function encode(str) { 3067 isEncoded = true; 3068 3069 return encodingLookup[str]; 3070 } 3071 3072 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 3073 // It will also decode the \" \' if keep_slashes is set to fale or omitted 3074 function decode(str, keep_slashes) { 3075 if (isEncoded) { 3076 str = str.replace(/\uFEFF[0-9]/g, function(str) { 3077 return encodingLookup[str]; 3078 }); 3079 } 3080 3081 if (!keep_slashes) { 3082 str = str.replace(/\\([\'\";:])/g, "$1"); 3083 } 3084 3085 return str; 3086 } 3087 3088 function processUrl(match, url, url2, url3, str, str2) { 3089 str = str || str2; 3090 3091 if (str) { 3092 str = decode(str); 3093 3094 // Force strings into single quote format 3095 return "'" + str.replace(/\'/g, "\\'") + "'"; 3096 } 3097 3098 url = decode(url || url2 || url3); 3099 3100 if (!settings.allow_script_urls) { 3101 var scriptUrl = url.replace(/[\s\r\n]+/, ''); 3102 3103 if (/(java|vb)script:/i.test(scriptUrl)) { 3104 return ""; 3105 } 3106 3107 if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { 3108 return ""; 3109 } 3110 } 3111 3112 // Convert the URL to relative/absolute depending on config 3113 if (urlConverter) { 3114 url = urlConverter.call(urlConverterScope, url, 'style'); 3115 } 3116 3117 // Output new URL format 3118 return "url('" + url.replace(/\'/g, "\\'") + "')"; 3119 } 3120 3121 if (css) { 3122 css = css.replace(/[\u0000-\u001F]/g, ''); 3123 3124 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 3125 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { 3126 return str.replace(/[;:]/g, encode); 3127 }); 3128 3129 // Parse styles 3130 while ((matches = styleRegExp.exec(css))) { 3131 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); 3132 value = matches[2].replace(trimRightRegExp, ''); 3133 3134 // Decode escaped sequences like \65 -> e 3135 /*jshint loopfunc:true*/ 3136 /*eslint no-loop-func:0 */ 3137 value = value.replace(/\\[0-9a-f]+/g, function(e) { 3138 return String.fromCharCode(parseInt(e.substr(1), 16)); 3139 }); 3140 3141 if (name && value.length > 0) { 3142 // Don't allow behavior name or expression/comments within the values 3143 if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { 3144 continue; 3145 } 3146 3147 // Opera will produce 700 instead of bold in their style values 3148 if (name === 'font-weight' && value === '700') { 3149 value = 'bold'; 3150 } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED 3151 value = value.toLowerCase(); 3152 } 3153 3154 // Convert RGB colors to HEX 3155 value = value.replace(rgbRegExp, toHex); 3156 3157 // Convert URLs and force them into url('value') format 3158 value = value.replace(urlOrStrRegExp, processUrl); 3159 styles[name] = isEncoded ? decode(value, true) : value; 3160 } 3161 3162 styleRegExp.lastIndex = matches.index + matches[0].length; 3163 } 3164 // Compress the styles to reduce it's size for example IE will expand styles 3165 compress("border", "", true); 3166 compress("border", "-width"); 3167 compress("border", "-color"); 3168 compress("border", "-style"); 3169 compress("padding", ""); 3170 compress("margin", ""); 3171 compress2('border', 'border-width', 'border-style', 'border-color'); 3172 3173 // Remove pointless border, IE produces these 3174 if (styles.border === 'medium none') { 3175 delete styles.border; 3176 } 3177 3178 // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p> 3179 // So lets asume it shouldn't be there 3180 if (styles['border-image'] === 'none') { 3181 delete styles['border-image']; 3182 } 3183 } 3184 3185 return styles; 3186 }, 3187 3188 /** 3189 * Serializes the specified style object into a string. 3190 * 3191 * @method serialize 3192 * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} 3193 * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized. 3194 * @return {String} String representation of the style object for example: border: 1px solid red. 3195 */ 3196 serialize: function(styles, elementName) { 3197 var css = '', name, value; 3198 3199 function serializeStyles(name) { 3200 var styleList, i, l, value; 3201 3202 styleList = validStyles[name]; 3203 if (styleList) { 3204 for (i = 0, l = styleList.length; i < l; i++) { 3205 name = styleList[i]; 3206 value = styles[name]; 3207 3208 if (value !== undef && value.length > 0) { 3209 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 3210 } 3211 } 3212 } 3213 } 3214 3215 function isValid(name, elementName) { 3216 var styleMap; 3217 3218 styleMap = invalidStyles['*']; 3219 if (styleMap && styleMap[name]) { 3220 return false; 3221 } 3222 3223 styleMap = invalidStyles[elementName]; 3224 if (styleMap && styleMap[name]) { 3225 return false; 3226 } 3227 3228 return true; 3229 } 3230 3231 // Serialize styles according to schema 3232 if (elementName && validStyles) { 3233 // Serialize global styles and element specific styles 3234 serializeStyles('*'); 3235 serializeStyles(elementName); 3236 } else { 3237 // Output the styles in the order they are inside the object 3238 for (name in styles) { 3239 value = styles[name]; 3240 3241 if (value !== undef && value.length > 0) { 3242 if (!invalidStyles || isValid(name, elementName)) { 3243 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 3244 } 3245 } 3246 } 3247 } 3248 3249 return css; 3250 } 3251 }; 3252 }; 3253 }); 3254 3255 // Included from: js/tinymce/classes/dom/TreeWalker.js 3256 3257 /** 3258 * TreeWalker.js 3259 * 3260 * Copyright, Moxiecode Systems AB 3261 * Released under LGPL License. 3262 * 3263 * License: http://www.tinymce.com/license 3264 * Contributing: http://www.tinymce.com/contributing 3265 */ 3266 3267 /** 3268 * TreeWalker class enables you to walk the DOM in a linear manner. 3269 * 3270 * @class tinymce.dom.TreeWalker 3271 * @example 3272 * var walker = new tinymce.dom.TreeWalker(startNode); 3273 * 3274 * do { 3275 * console.log(walker.current()); 3276 * } while (walker.next()); 3277 */ 3278 define("tinymce/dom/TreeWalker", [], function() { 3279 /** 3280 * Constructs a new TreeWalker instance. 3281 * 3282 * @constructor 3283 * @method TreeWalker 3284 * @param {Node} startNode Node to start walking from. 3285 * @param {node} rootNode Optional root node to never walk out of. 3286 */ 3287 return function(startNode, rootNode) { 3288 var node = startNode; 3289 3290 function findSibling(node, startName, siblingName, shallow) { 3291 var sibling, parent; 3292 3293 if (node) { 3294 // Walk into nodes if it has a start 3295 if (!shallow && node[startName]) { 3296 return node[startName]; 3297 } 3298 3299 // Return the sibling if it has one 3300 if (node != rootNode) { 3301 sibling = node[siblingName]; 3302 if (sibling) { 3303 return sibling; 3304 } 3305 3306 // Walk up the parents to look for siblings 3307 for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) { 3308 sibling = parent[siblingName]; 3309 if (sibling) { 3310 return sibling; 3311 } 3312 } 3313 } 3314 } 3315 } 3316 3317 /** 3318 * Returns the current node. 3319 * 3320 * @method current 3321 * @return {Node} Current node where the walker is. 3322 */ 3323 this.current = function() { 3324 return node; 3325 }; 3326 3327 /** 3328 * Walks to the next node in tree. 3329 * 3330 * @method next 3331 * @return {Node} Current node where the walker is after moving to the next node. 3332 */ 3333 this.next = function(shallow) { 3334 node = findSibling(node, 'firstChild', 'nextSibling', shallow); 3335 return node; 3336 }; 3337 3338 /** 3339 * Walks to the previous node in tree. 3340 * 3341 * @method prev 3342 * @return {Node} Current node where the walker is after moving to the previous node. 3343 */ 3344 this.prev = function(shallow) { 3345 node = findSibling(node, 'lastChild', 'previousSibling', shallow); 3346 return node; 3347 }; 3348 }; 3349 }); 3350 3351 // Included from: js/tinymce/classes/dom/Range.js 3352 3353 /** 3354 * Range.js 3355 * 3356 * Copyright, Moxiecode Systems AB 3357 * Released under LGPL License. 3358 * 3359 * License: http://www.tinymce.com/license 3360 * Contributing: http://www.tinymce.com/contributing 3361 */ 3362 3363 define("tinymce/dom/Range", [ 3364 "tinymce/util/Tools" 3365 ], function(Tools) { 3366 // Range constructor 3367 function Range(dom) { 3368 var self = this, 3369 doc = dom.doc, 3370 EXTRACT = 0, 3371 CLONE = 1, 3372 DELETE = 2, 3373 TRUE = true, 3374 FALSE = false, 3375 START_OFFSET = 'startOffset', 3376 START_CONTAINER = 'startContainer', 3377 END_CONTAINER = 'endContainer', 3378 END_OFFSET = 'endOffset', 3379 extend = Tools.extend, 3380 nodeIndex = dom.nodeIndex; 3381 3382 function createDocumentFragment() { 3383 return doc.createDocumentFragment(); 3384 } 3385 3386 function setStart(n, o) { 3387 _setEndPoint(TRUE, n, o); 3388 } 3389 3390 function setEnd(n, o) { 3391 _setEndPoint(FALSE, n, o); 3392 } 3393 3394 function setStartBefore(n) { 3395 setStart(n.parentNode, nodeIndex(n)); 3396 } 3397 3398 function setStartAfter(n) { 3399 setStart(n.parentNode, nodeIndex(n) + 1); 3400 } 3401 3402 function setEndBefore(n) { 3403 setEnd(n.parentNode, nodeIndex(n)); 3404 } 3405 3406 function setEndAfter(n) { 3407 setEnd(n.parentNode, nodeIndex(n) + 1); 3408 } 3409 3410 function collapse(ts) { 3411 if (ts) { 3412 self[END_CONTAINER] = self[START_CONTAINER]; 3413 self[END_OFFSET] = self[START_OFFSET]; 3414 } else { 3415 self[START_CONTAINER] = self[END_CONTAINER]; 3416 self[START_OFFSET] = self[END_OFFSET]; 3417 } 3418 3419 self.collapsed = TRUE; 3420 } 3421 3422 function selectNode(n) { 3423 setStartBefore(n); 3424 setEndAfter(n); 3425 } 3426 3427 function selectNodeContents(n) { 3428 setStart(n, 0); 3429 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); 3430 } 3431 3432 function compareBoundaryPoints(h, r) { 3433 var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET], 3434 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; 3435 3436 // Check START_TO_START 3437 if (h === 0) { 3438 return _compareBoundaryPoints(sc, so, rsc, rso); 3439 } 3440 3441 // Check START_TO_END 3442 if (h === 1) { 3443 return _compareBoundaryPoints(ec, eo, rsc, rso); 3444 } 3445 3446 // Check END_TO_END 3447 if (h === 2) { 3448 return _compareBoundaryPoints(ec, eo, rec, reo); 3449 } 3450 3451 // Check END_TO_START 3452 if (h === 3) { 3453 return _compareBoundaryPoints(sc, so, rec, reo); 3454 } 3455 } 3456 3457 function deleteContents() { 3458 _traverse(DELETE); 3459 } 3460 3461 function extractContents() { 3462 return _traverse(EXTRACT); 3463 } 3464 3465 function cloneContents() { 3466 return _traverse(CLONE); 3467 } 3468 3469 function insertNode(n) { 3470 var startContainer = this[START_CONTAINER], 3471 startOffset = this[START_OFFSET], nn, o; 3472 3473 // Node is TEXT_NODE or CDATA 3474 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { 3475 if (!startOffset) { 3476 // At the start of text 3477 startContainer.parentNode.insertBefore(n, startContainer); 3478 } else if (startOffset >= startContainer.nodeValue.length) { 3479 // At the end of text 3480 dom.insertAfter(n, startContainer); 3481 } else { 3482 // Middle, need to split 3483 nn = startContainer.splitText(startOffset); 3484 startContainer.parentNode.insertBefore(n, nn); 3485 } 3486 } else { 3487 // Insert element node 3488 if (startContainer.childNodes.length > 0) { 3489 o = startContainer.childNodes[startOffset]; 3490 } 3491 3492 if (o) { 3493 startContainer.insertBefore(n, o); 3494 } else { 3495 if (startContainer.nodeType == 3) { 3496 dom.insertAfter(n, startContainer); 3497 } else { 3498 startContainer.appendChild(n); 3499 } 3500 } 3501 } 3502 } 3503 3504 function surroundContents(n) { 3505 var f = self.extractContents(); 3506 3507 self.insertNode(n); 3508 n.appendChild(f); 3509 self.selectNode(n); 3510 } 3511 3512 function cloneRange() { 3513 return extend(new Range(dom), { 3514 startContainer: self[START_CONTAINER], 3515 startOffset: self[START_OFFSET], 3516 endContainer: self[END_CONTAINER], 3517 endOffset: self[END_OFFSET], 3518 collapsed: self.collapsed, 3519 commonAncestorContainer: self.commonAncestorContainer 3520 }); 3521 } 3522 3523 // Private methods 3524 3525 function _getSelectedNode(container, offset) { 3526 var child; 3527 3528 if (container.nodeType == 3 /* TEXT_NODE */) { 3529 return container; 3530 } 3531 3532 if (offset < 0) { 3533 return container; 3534 } 3535 3536 child = container.firstChild; 3537 while (child && offset > 0) { 3538 --offset; 3539 child = child.nextSibling; 3540 } 3541 3542 if (child) { 3543 return child; 3544 } 3545 3546 return container; 3547 } 3548 3549 function _isCollapsed() { 3550 return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]); 3551 } 3552 3553 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { 3554 var c, offsetC, n, cmnRoot, childA, childB; 3555 3556 // In the first case the boundary-points have the same container. A is before B 3557 // if its offset is less than the offset of B, A is equal to B if its offset is 3558 // equal to the offset of B, and A is after B if its offset is greater than the 3559 // offset of B. 3560 if (containerA == containerB) { 3561 if (offsetA == offsetB) { 3562 return 0; // equal 3563 } 3564 3565 if (offsetA < offsetB) { 3566 return -1; // before 3567 } 3568 3569 return 1; // after 3570 } 3571 3572 // In the second case a child node C of the container of A is an ancestor 3573 // container of B. In this case, A is before B if the offset of A is less than or 3574 // equal to the index of the child node C and A is after B otherwise. 3575 c = containerB; 3576 while (c && c.parentNode != containerA) { 3577 c = c.parentNode; 3578 } 3579 3580 if (c) { 3581 offsetC = 0; 3582 n = containerA.firstChild; 3583 3584 while (n != c && offsetC < offsetA) { 3585 offsetC++; 3586 n = n.nextSibling; 3587 } 3588 3589 if (offsetA <= offsetC) { 3590 return -1; // before 3591 } 3592 3593 return 1; // after 3594 } 3595 3596 // In the third case a child node C of the container of B is an ancestor container 3597 // of A. In this case, A is before B if the index of the child node C is less than 3598 // the offset of B and A is after B otherwise. 3599 c = containerA; 3600 while (c && c.parentNode != containerB) { 3601 c = c.parentNode; 3602 } 3603 3604 if (c) { 3605 offsetC = 0; 3606 n = containerB.firstChild; 3607 3608 while (n != c && offsetC < offsetB) { 3609 offsetC++; 3610 n = n.nextSibling; 3611 } 3612 3613 if (offsetC < offsetB) { 3614 return -1; // before 3615 } 3616 3617 return 1; // after 3618 } 3619 3620 // In the fourth case, none of three other cases hold: the containers of A and B 3621 // are siblings or descendants of sibling nodes. In this case, A is before B if 3622 // the container of A is before the container of B in a pre-order traversal of the 3623 // Ranges' context tree and A is after B otherwise. 3624 cmnRoot = dom.findCommonAncestor(containerA, containerB); 3625 childA = containerA; 3626 3627 while (childA && childA.parentNode != cmnRoot) { 3628 childA = childA.parentNode; 3629 } 3630 3631 if (!childA) { 3632 childA = cmnRoot; 3633 } 3634 3635 childB = containerB; 3636 while (childB && childB.parentNode != cmnRoot) { 3637 childB = childB.parentNode; 3638 } 3639 3640 if (!childB) { 3641 childB = cmnRoot; 3642 } 3643 3644 if (childA == childB) { 3645 return 0; // equal 3646 } 3647 3648 n = cmnRoot.firstChild; 3649 while (n) { 3650 if (n == childA) { 3651 return -1; // before 3652 } 3653 3654 if (n == childB) { 3655 return 1; // after 3656 } 3657 3658 n = n.nextSibling; 3659 } 3660 } 3661 3662 function _setEndPoint(st, n, o) { 3663 var ec, sc; 3664 3665 if (st) { 3666 self[START_CONTAINER] = n; 3667 self[START_OFFSET] = o; 3668 } else { 3669 self[END_CONTAINER] = n; 3670 self[END_OFFSET] = o; 3671 } 3672 3673 // If one boundary-point of a Range is set to have a root container 3674 // other than the current one for the Range, the Range is collapsed to 3675 // the new position. This enforces the restriction that both boundary- 3676 // points of a Range must have the same root container. 3677 ec = self[END_CONTAINER]; 3678 while (ec.parentNode) { 3679 ec = ec.parentNode; 3680 } 3681 3682 sc = self[START_CONTAINER]; 3683 while (sc.parentNode) { 3684 sc = sc.parentNode; 3685 } 3686 3687 if (sc == ec) { 3688 // The start position of a Range is guaranteed to never be after the 3689 // end position. To enforce this restriction, if the start is set to 3690 // be at a position after the end, the Range is collapsed to that 3691 // position. 3692 if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) { 3693 self.collapse(st); 3694 } 3695 } else { 3696 self.collapse(st); 3697 } 3698 3699 self.collapsed = _isCollapsed(); 3700 self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]); 3701 } 3702 3703 function _traverse(how) { 3704 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; 3705 3706 if (self[START_CONTAINER] == self[END_CONTAINER]) { 3707 return _traverseSameContainer(how); 3708 } 3709 3710 for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 3711 if (p == self[START_CONTAINER]) { 3712 return _traverseCommonStartContainer(c, how); 3713 } 3714 3715 ++endContainerDepth; 3716 } 3717 3718 for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 3719 if (p == self[END_CONTAINER]) { 3720 return _traverseCommonEndContainer(c, how); 3721 } 3722 3723 ++startContainerDepth; 3724 } 3725 3726 depthDiff = startContainerDepth - endContainerDepth; 3727 3728 startNode = self[START_CONTAINER]; 3729 while (depthDiff > 0) { 3730 startNode = startNode.parentNode; 3731 depthDiff--; 3732 } 3733 3734 endNode = self[END_CONTAINER]; 3735 while (depthDiff < 0) { 3736 endNode = endNode.parentNode; 3737 depthDiff++; 3738 } 3739 3740 // ascend the ancestor hierarchy until we have a common parent. 3741 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { 3742 startNode = sp; 3743 endNode = ep; 3744 } 3745 3746 return _traverseCommonAncestors(startNode, endNode, how); 3747 } 3748 3749 function _traverseSameContainer(how) { 3750 var frag, s, sub, n, cnt, sibling, xferNode, start, len; 3751 3752 if (how != DELETE) { 3753 frag = createDocumentFragment(); 3754 } 3755 3756 // If selection is empty, just return the fragment 3757 if (self[START_OFFSET] == self[END_OFFSET]) { 3758 return frag; 3759 } 3760 3761 // Text node needs special case handling 3762 if (self[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { 3763 // get the substring 3764 s = self[START_CONTAINER].nodeValue; 3765 sub = s.substring(self[START_OFFSET], self[END_OFFSET]); 3766 3767 // set the original text node to its new value 3768 if (how != CLONE) { 3769 n = self[START_CONTAINER]; 3770 start = self[START_OFFSET]; 3771 len = self[END_OFFSET] - self[START_OFFSET]; 3772 3773 if (start === 0 && len >= n.nodeValue.length - 1) { 3774 n.parentNode.removeChild(n); 3775 } else { 3776 n.deleteData(start, len); 3777 } 3778 3779 // Nothing is partially selected, so collapse to start point 3780 self.collapse(TRUE); 3781 } 3782 3783 if (how == DELETE) { 3784 return; 3785 } 3786 3787 if (sub.length > 0) { 3788 frag.appendChild(doc.createTextNode(sub)); 3789 } 3790 3791 return frag; 3792 } 3793 3794 // Copy nodes between the start/end offsets. 3795 n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]); 3796 cnt = self[END_OFFSET] - self[START_OFFSET]; 3797 3798 while (n && cnt > 0) { 3799 sibling = n.nextSibling; 3800 xferNode = _traverseFullySelected(n, how); 3801 3802 if (frag) { 3803 frag.appendChild(xferNode); 3804 } 3805 3806 --cnt; 3807 n = sibling; 3808 } 3809 3810 // Nothing is partially selected, so collapse to start point 3811 if (how != CLONE) { 3812 self.collapse(TRUE); 3813 } 3814 3815 return frag; 3816 } 3817 3818 function _traverseCommonStartContainer(endAncestor, how) { 3819 var frag, n, endIdx, cnt, sibling, xferNode; 3820 3821 if (how != DELETE) { 3822 frag = createDocumentFragment(); 3823 } 3824 3825 n = _traverseRightBoundary(endAncestor, how); 3826 3827 if (frag) { 3828 frag.appendChild(n); 3829 } 3830 3831 endIdx = nodeIndex(endAncestor); 3832 cnt = endIdx - self[START_OFFSET]; 3833 3834 if (cnt <= 0) { 3835 // Collapse to just before the endAncestor, which 3836 // is partially selected. 3837 if (how != CLONE) { 3838 self.setEndBefore(endAncestor); 3839 self.collapse(FALSE); 3840 } 3841 3842 return frag; 3843 } 3844 3845 n = endAncestor.previousSibling; 3846 while (cnt > 0) { 3847 sibling = n.previousSibling; 3848 xferNode = _traverseFullySelected(n, how); 3849 3850 if (frag) { 3851 frag.insertBefore(xferNode, frag.firstChild); 3852 } 3853 3854 --cnt; 3855 n = sibling; 3856 } 3857 3858 // Collapse to just before the endAncestor, which 3859 // is partially selected. 3860 if (how != CLONE) { 3861 self.setEndBefore(endAncestor); 3862 self.collapse(FALSE); 3863 } 3864 3865 return frag; 3866 } 3867 3868 function _traverseCommonEndContainer(startAncestor, how) { 3869 var frag, startIdx, n, cnt, sibling, xferNode; 3870 3871 if (how != DELETE) { 3872 frag = createDocumentFragment(); 3873 } 3874 3875 n = _traverseLeftBoundary(startAncestor, how); 3876 if (frag) { 3877 frag.appendChild(n); 3878 } 3879 3880 startIdx = nodeIndex(startAncestor); 3881 ++startIdx; // Because we already traversed it 3882 3883 cnt = self[END_OFFSET] - startIdx; 3884 n = startAncestor.nextSibling; 3885 while (n && cnt > 0) { 3886 sibling = n.nextSibling; 3887 xferNode = _traverseFullySelected(n, how); 3888 3889 if (frag) { 3890 frag.appendChild(xferNode); 3891 } 3892 3893 --cnt; 3894 n = sibling; 3895 } 3896 3897 if (how != CLONE) { 3898 self.setStartAfter(startAncestor); 3899 self.collapse(TRUE); 3900 } 3901 3902 return frag; 3903 } 3904 3905 function _traverseCommonAncestors(startAncestor, endAncestor, how) { 3906 var n, frag, startOffset, endOffset, cnt, sibling, nextSibling; 3907 3908 if (how != DELETE) { 3909 frag = createDocumentFragment(); 3910 } 3911 3912 n = _traverseLeftBoundary(startAncestor, how); 3913 if (frag) { 3914 frag.appendChild(n); 3915 } 3916 3917 startOffset = nodeIndex(startAncestor); 3918 endOffset = nodeIndex(endAncestor); 3919 ++startOffset; 3920 3921 cnt = endOffset - startOffset; 3922 sibling = startAncestor.nextSibling; 3923 3924 while (cnt > 0) { 3925 nextSibling = sibling.nextSibling; 3926 n = _traverseFullySelected(sibling, how); 3927 3928 if (frag) { 3929 frag.appendChild(n); 3930 } 3931 3932 sibling = nextSibling; 3933 --cnt; 3934 } 3935 3936 n = _traverseRightBoundary(endAncestor, how); 3937 3938 if (frag) { 3939 frag.appendChild(n); 3940 } 3941 3942 if (how != CLONE) { 3943 self.setStartAfter(startAncestor); 3944 self.collapse(TRUE); 3945 } 3946 3947 return frag; 3948 } 3949 3950 function _traverseRightBoundary(root, how) { 3951 var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent; 3952 var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER]; 3953 3954 if (next == root) { 3955 return _traverseNode(next, isFullySelected, FALSE, how); 3956 } 3957 3958 parent = next.parentNode; 3959 clonedParent = _traverseNode(parent, FALSE, FALSE, how); 3960 3961 while (parent) { 3962 while (next) { 3963 prevSibling = next.previousSibling; 3964 clonedChild = _traverseNode(next, isFullySelected, FALSE, how); 3965 3966 if (how != DELETE) { 3967 clonedParent.insertBefore(clonedChild, clonedParent.firstChild); 3968 } 3969 3970 isFullySelected = TRUE; 3971 next = prevSibling; 3972 } 3973 3974 if (parent == root) { 3975 return clonedParent; 3976 } 3977 3978 next = parent.previousSibling; 3979 parent = parent.parentNode; 3980 3981 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); 3982 3983 if (how != DELETE) { 3984 clonedGrandParent.appendChild(clonedParent); 3985 } 3986 3987 clonedParent = clonedGrandParent; 3988 } 3989 } 3990 3991 function _traverseLeftBoundary(root, how) { 3992 var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER]; 3993 var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; 3994 3995 if (next == root) { 3996 return _traverseNode(next, isFullySelected, TRUE, how); 3997 } 3998 3999 parent = next.parentNode; 4000 clonedParent = _traverseNode(parent, FALSE, TRUE, how); 4001 4002 while (parent) { 4003 while (next) { 4004 nextSibling = next.nextSibling; 4005 clonedChild = _traverseNode(next, isFullySelected, TRUE, how); 4006 4007 if (how != DELETE) { 4008 clonedParent.appendChild(clonedChild); 4009 } 4010 4011 isFullySelected = TRUE; 4012 next = nextSibling; 4013 } 4014 4015 if (parent == root) { 4016 return clonedParent; 4017 } 4018 4019 next = parent.nextSibling; 4020 parent = parent.parentNode; 4021 4022 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); 4023 4024 if (how != DELETE) { 4025 clonedGrandParent.appendChild(clonedParent); 4026 } 4027 4028 clonedParent = clonedGrandParent; 4029 } 4030 } 4031 4032 function _traverseNode(n, isFullySelected, isLeft, how) { 4033 var txtValue, newNodeValue, oldNodeValue, offset, newNode; 4034 4035 if (isFullySelected) { 4036 return _traverseFullySelected(n, how); 4037 } 4038 4039 if (n.nodeType == 3 /* TEXT_NODE */) { 4040 txtValue = n.nodeValue; 4041 4042 if (isLeft) { 4043 offset = self[START_OFFSET]; 4044 newNodeValue = txtValue.substring(offset); 4045 oldNodeValue = txtValue.substring(0, offset); 4046 } else { 4047 offset = self[END_OFFSET]; 4048 newNodeValue = txtValue.substring(0, offset); 4049 oldNodeValue = txtValue.substring(offset); 4050 } 4051 4052 if (how != CLONE) { 4053 n.nodeValue = oldNodeValue; 4054 } 4055 4056 if (how == DELETE) { 4057 return; 4058 } 4059 4060 newNode = dom.clone(n, FALSE); 4061 newNode.nodeValue = newNodeValue; 4062 4063 return newNode; 4064 } 4065 4066 if (how == DELETE) { 4067 return; 4068 } 4069 4070 return dom.clone(n, FALSE); 4071 } 4072 4073 function _traverseFullySelected(n, how) { 4074 if (how != DELETE) { 4075 return how == CLONE ? dom.clone(n, TRUE) : n; 4076 } 4077 4078 n.parentNode.removeChild(n); 4079 } 4080 4081 function toStringIE() { 4082 return dom.create('body', null, cloneContents()).outerText; 4083 } 4084 4085 extend(self, { 4086 // Inital states 4087 startContainer: doc, 4088 startOffset: 0, 4089 endContainer: doc, 4090 endOffset: 0, 4091 collapsed: TRUE, 4092 commonAncestorContainer: doc, 4093 4094 // Range constants 4095 START_TO_START: 0, 4096 START_TO_END: 1, 4097 END_TO_END: 2, 4098 END_TO_START: 3, 4099 4100 // Public methods 4101 setStart: setStart, 4102 setEnd: setEnd, 4103 setStartBefore: setStartBefore, 4104 setStartAfter: setStartAfter, 4105 setEndBefore: setEndBefore, 4106 setEndAfter: setEndAfter, 4107 collapse: collapse, 4108 selectNode: selectNode, 4109 selectNodeContents: selectNodeContents, 4110 compareBoundaryPoints: compareBoundaryPoints, 4111 deleteContents: deleteContents, 4112 extractContents: extractContents, 4113 cloneContents: cloneContents, 4114 insertNode: insertNode, 4115 surroundContents: surroundContents, 4116 cloneRange: cloneRange, 4117 toStringIE: toStringIE 4118 }); 4119 4120 return self; 4121 } 4122 4123 // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype 4124 Range.prototype.toString = function() { 4125 return this.toStringIE(); 4126 }; 4127 4128 return Range; 4129 }); 4130 4131 // Included from: js/tinymce/classes/html/Entities.js 4132 4133 /** 4134 * Entities.js 4135 * 4136 * Copyright, Moxiecode Systems AB 4137 * Released under LGPL License. 4138 * 4139 * License: http://www.tinymce.com/license 4140 * Contributing: http://www.tinymce.com/contributing 4141 */ 4142 4143 /*jshint bitwise:false */ 4144 /*eslint no-bitwise:0 */ 4145 4146 /** 4147 * Entity encoder class. 4148 * 4149 * @class tinymce.html.Entities 4150 * @static 4151 * @version 3.4 4152 */ 4153 define("tinymce/html/Entities", [ 4154 "tinymce/util/Tools" 4155 ], function(Tools) { 4156 var makeMap = Tools.makeMap; 4157 4158 var namedEntities, baseEntities, reverseEntities, 4159 attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 4160 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 4161 rawCharsRegExp = /[<>&\"\']/g, 4162 entityRegExp = /&(#x|#)?([\w]+);/g, 4163 asciiMap = { 4164 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020", 4165 135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152", 4166 142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022", 4167 150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A", 4168 156: "\u0153", 158: "\u017E", 159: "\u0178" 4169 }; 4170 4171 // Raw entities 4172 baseEntities = { 4173 '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code 4174 "'": ''', 4175 '<': '<', 4176 '>': '>', 4177 '&': '&', 4178 '\u0060': '`' 4179 }; 4180 4181 // Reverse lookup table for raw entities 4182 reverseEntities = { 4183 '<': '<', 4184 '>': '>', 4185 '&': '&', 4186 '"': '"', 4187 ''': "'" 4188 }; 4189 4190 // Decodes text by using the browser 4191 function nativeDecode(text) { 4192 var elm; 4193 4194 elm = document.createElement("div"); 4195 elm.innerHTML = text; 4196 4197 return elm.textContent || elm.innerText || text; 4198 } 4199 4200 // Build a two way lookup table for the entities 4201 function buildEntitiesLookup(items, radix) { 4202 var i, chr, entity, lookup = {}; 4203 4204 if (items) { 4205 items = items.split(','); 4206 radix = radix || 10; 4207 4208 // Build entities lookup table 4209 for (i = 0; i < items.length; i += 2) { 4210 chr = String.fromCharCode(parseInt(items[i], radix)); 4211 4212 // Only add non base entities 4213 if (!baseEntities[chr]) { 4214 entity = '&' + items[i + 1] + ';'; 4215 lookup[chr] = entity; 4216 lookup[entity] = chr; 4217 } 4218 } 4219 4220 return lookup; 4221 } 4222 } 4223 4224 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 4225 namedEntities = buildEntitiesLookup( 4226 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 4227 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 4228 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 4229 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 4230 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 4231 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 4232 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 4233 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 4234 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 4235 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 4236 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 4237 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 4238 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 4239 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 4240 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 4241 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 4242 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 4243 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 4244 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 4245 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 4246 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 4247 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 4248 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 4249 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 4250 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 4251 4252 var Entities = { 4253 /** 4254 * Encodes the specified string using raw entities. This means only the required XML base entities will be endoded. 4255 * 4256 * @method encodeRaw 4257 * @param {String} text Text to encode. 4258 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 4259 * @return {String} Entity encoded text. 4260 */ 4261 encodeRaw: function(text, attr) { 4262 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 4263 return baseEntities[chr] || chr; 4264 }); 4265 }, 4266 4267 /** 4268 * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents 4269 * since it doesn't know if the context is within a attribute or text node. This was added for compatibility 4270 * and is exposed as the DOMUtils.encode function. 4271 * 4272 * @method encodeAllRaw 4273 * @param {String} text Text to encode. 4274 * @return {String} Entity encoded text. 4275 */ 4276 encodeAllRaw: function(text) { 4277 return ('' + text).replace(rawCharsRegExp, function(chr) { 4278 return baseEntities[chr] || chr; 4279 }); 4280 }, 4281 4282 /** 4283 * Encodes the specified string using numeric entities. The core entities will be 4284 * encoded as named ones but all non lower ascii characters will be encoded into numeric entities. 4285 * 4286 * @method encodeNumeric 4287 * @param {String} text Text to encode. 4288 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 4289 * @return {String} Entity encoded text. 4290 */ 4291 encodeNumeric: function(text, attr) { 4292 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 4293 // Multi byte sequence convert it to a single entity 4294 if (chr.length > 1) { 4295 return '' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 4296 } 4297 4298 return baseEntities[chr] || '' + chr.charCodeAt(0) + ';'; 4299 }); 4300 }, 4301 4302 /** 4303 * Encodes the specified string using named entities. The core entities will be encoded 4304 * as named ones but all non lower ascii characters will be encoded into named entities. 4305 * 4306 * @method encodeNamed 4307 * @param {String} text Text to encode. 4308 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 4309 * @param {Object} entities Optional parameter with entities to use. 4310 * @return {String} Entity encoded text. 4311 */ 4312 encodeNamed: function(text, attr, entities) { 4313 entities = entities || namedEntities; 4314 4315 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 4316 return baseEntities[chr] || entities[chr] || chr; 4317 }); 4318 }, 4319 4320 /** 4321 * Returns an encode function based on the name(s) and it's optional entities. 4322 * 4323 * @method getEncodeFunc 4324 * @param {String} name Comma separated list of encoders for example named,numeric. 4325 * @param {String} entities Optional parameter with entities to use instead of the built in set. 4326 * @return {function} Encode function to be used. 4327 */ 4328 getEncodeFunc: function(name, entities) { 4329 entities = buildEntitiesLookup(entities) || namedEntities; 4330 4331 function encodeNamedAndNumeric(text, attr) { 4332 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 4333 return baseEntities[chr] || entities[chr] || '' + chr.charCodeAt(0) + ';' || chr; 4334 }); 4335 } 4336 4337 function encodeCustomNamed(text, attr) { 4338 return Entities.encodeNamed(text, attr, entities); 4339 } 4340 4341 // Replace + with , to be compatible with previous TinyMCE versions 4342 name = makeMap(name.replace(/\+/g, ',')); 4343 4344 // Named and numeric encoder 4345 if (name.named && name.numeric) { 4346 return encodeNamedAndNumeric; 4347 } 4348 4349 // Named encoder 4350 if (name.named) { 4351 // Custom names 4352 if (entities) { 4353 return encodeCustomNamed; 4354 } 4355 4356 return Entities.encodeNamed; 4357 } 4358 4359 // Numeric 4360 if (name.numeric) { 4361 return Entities.encodeNumeric; 4362 } 4363 4364 // Raw encoder 4365 return Entities.encodeRaw; 4366 }, 4367 4368 /** 4369 * Decodes the specified string, this will replace entities with raw UTF characters. 4370 * 4371 * @method decode 4372 * @param {String} text Text to entity decode. 4373 * @return {String} Entity decoded string. 4374 */ 4375 decode: function(text) { 4376 return text.replace(entityRegExp, function(all, numeric, value) { 4377 if (numeric) { 4378 value = parseInt(value, numeric.length === 2 ? 16 : 10); 4379 4380 // Support upper UTF 4381 if (value > 0xFFFF) { 4382 value -= 0x10000; 4383 4384 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); 4385 } else { 4386 return asciiMap[value] || String.fromCharCode(value); 4387 } 4388 } 4389 4390 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 4391 }); 4392 } 4393 }; 4394 4395 return Entities; 4396 }); 4397 4398 // Included from: js/tinymce/classes/dom/StyleSheetLoader.js 4399 4400 /** 4401 * StyleSheetLoader.js 4402 * 4403 * Copyright, Moxiecode Systems AB 4404 * Released under LGPL License. 4405 * 4406 * License: http://www.tinymce.com/license 4407 * Contributing: http://www.tinymce.com/contributing 4408 */ 4409 4410 /** 4411 * This class handles loading of external stylesheets and fires events when these are loaded. 4412 * 4413 * @class tinymce.dom.StyleSheetLoader 4414 * @private 4415 */ 4416 define("tinymce/dom/StyleSheetLoader", [], function() { 4417 "use strict"; 4418 4419 return function(document, settings) { 4420 var idCount = 0, loadedStates = {}, maxLoadTime; 4421 4422 settings = settings || {}; 4423 maxLoadTime = settings.maxLoadTime || 5000; 4424 4425 function appendToHead(node) { 4426 document.getElementsByTagName('head')[0].appendChild(node); 4427 } 4428 4429 /** 4430 * Loads the specified css style sheet file and call the loadedCallback once it's finished loading. 4431 * 4432 * @method load 4433 * @param {String} url Url to be loaded. 4434 * @param {Function} loadedCallback Callback to be executed when loaded. 4435 * @param {Function} errorCallback Callback to be executed when failed loading. 4436 */ 4437 function load(url, loadedCallback, errorCallback) { 4438 var link, style, startTime, state; 4439 4440 function passed() { 4441 var callbacks = state.passed, i = callbacks.length; 4442 4443 while (i--) { 4444 callbacks[i](); 4445 } 4446 4447 state.status = 2; 4448 state.passed = []; 4449 state.failed = []; 4450 } 4451 4452 function failed() { 4453 var callbacks = state.failed, i = callbacks.length; 4454 4455 while (i--) { 4456 callbacks[i](); 4457 } 4458 4459 state.status = 3; 4460 state.passed = []; 4461 state.failed = []; 4462 } 4463 4464 // Sniffs for older WebKit versions that have the link.onload but a broken one 4465 function isOldWebKit() { 4466 var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/); 4467 return !!(webKitChunks && webKitChunks[1] < 536); 4468 } 4469 4470 // Calls the waitCallback until the test returns true or the timeout occurs 4471 function wait(testCallback, waitCallback) { 4472 if (!testCallback()) { 4473 // Wait for timeout 4474 if ((new Date().getTime()) - startTime < maxLoadTime) { 4475 window.setTimeout(waitCallback, 0); 4476 } else { 4477 failed(); 4478 } 4479 } 4480 } 4481 4482 // Workaround for WebKit that doesn't properly support the onload event for link elements 4483 // Or WebKit that fires the onload event before the StyleSheet is added to the document 4484 function waitForWebKitLinkLoaded() { 4485 wait(function() { 4486 var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner; 4487 4488 while (i--) { 4489 styleSheet = styleSheets[i]; 4490 owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement; 4491 if (owner && owner.id === link.id) { 4492 passed(); 4493 return true; 4494 } 4495 } 4496 }, waitForWebKitLinkLoaded); 4497 } 4498 4499 // Workaround for older Geckos that doesn't have any onload event for StyleSheets 4500 function waitForGeckoLinkLoaded() { 4501 wait(function() { 4502 try { 4503 // Accessing the cssRules will throw an exception until the CSS file is loaded 4504 var cssRules = style.sheet.cssRules; 4505 passed(); 4506 return !!cssRules; 4507 } catch (ex) { 4508 // Ignore 4509 } 4510 }, waitForGeckoLinkLoaded); 4511 } 4512 4513 if (!loadedStates[url]) { 4514 state = { 4515 passed: [], 4516 failed: [] 4517 }; 4518 4519 loadedStates[url] = state; 4520 } else { 4521 state = loadedStates[url]; 4522 } 4523 4524 if (loadedCallback) { 4525 state.passed.push(loadedCallback); 4526 } 4527 4528 if (errorCallback) { 4529 state.failed.push(errorCallback); 4530 } 4531 4532 // Is loading wait for it to pass 4533 if (state.status == 1) { 4534 return; 4535 } 4536 4537 // Has finished loading and was success 4538 if (state.status == 2) { 4539 passed(); 4540 return; 4541 } 4542 4543 // Has finished loading and was a failure 4544 if (state.status == 3) { 4545 failed(); 4546 return; 4547 } 4548 4549 // Start loading 4550 state.status = 1; 4551 link = document.createElement('link'); 4552 link.rel = 'stylesheet'; 4553 link.type = 'text/css'; 4554 link.id = 'u' + (idCount++); 4555 link.async = false; 4556 link.defer = false; 4557 startTime = new Date().getTime(); 4558 4559 // Feature detect onload on link element and sniff older webkits since it has an broken onload event 4560 if ("onload" in link && !isOldWebKit()) { 4561 link.onload = waitForWebKitLinkLoaded; 4562 link.onerror = failed; 4563 } else { 4564 // Sniff for old Firefox that doesn't support the onload event on link elements 4565 // TODO: Remove this in the future when everyone uses modern browsers 4566 if (navigator.userAgent.indexOf("Firefox") > 0) { 4567 style = document.createElement('style'); 4568 style.textContent = '@import "' + url + '"'; 4569 waitForGeckoLinkLoaded(); 4570 appendToHead(style); 4571 return; 4572 } else { 4573 // Use the id owner on older webkits 4574 waitForWebKitLinkLoaded(); 4575 } 4576 } 4577 4578 appendToHead(link); 4579 link.href = url; 4580 } 4581 4582 this.load = load; 4583 }; 4584 }); 4585 4586 // Included from: js/tinymce/classes/dom/DOMUtils.js 4587 4588 /** 4589 * DOMUtils.js 4590 * 4591 * Copyright, Moxiecode Systems AB 4592 * Released under LGPL License. 4593 * 4594 * License: http://www.tinymce.com/license 4595 * Contributing: http://www.tinymce.com/contributing 4596 */ 4597 4598 /** 4599 * Utility class for various DOM manipulation and retrieval functions. 4600 * 4601 * @class tinymce.dom.DOMUtils 4602 * @example 4603 * // Add a class to an element by id in the page 4604 * tinymce.DOM.addClass('someid', 'someclass'); 4605 * 4606 * // Add a class to an element by id inside the editor 4607 * tinymce.activeEditor.dom.addClass('someid', 'someclass'); 4608 */ 4609 define("tinymce/dom/DOMUtils", [ 4610 "tinymce/dom/Sizzle", 4611 "tinymce/dom/DomQuery", 4612 "tinymce/html/Styles", 4613 "tinymce/dom/EventUtils", 4614 "tinymce/dom/TreeWalker", 4615 "tinymce/dom/Range", 4616 "tinymce/html/Entities", 4617 "tinymce/Env", 4618 "tinymce/util/Tools", 4619 "tinymce/dom/StyleSheetLoader" 4620 ], function(Sizzle, $, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) { 4621 // Shorten names 4622 var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim; 4623 var isIE = Env.ie; 4624 var simpleSelectorRe = /^([a-z0-9],?)+$/i; 4625 var whiteSpaceRegExp = /^[ \t\r\n]*$/; 4626 4627 function setupAttrHooks(domUtils, settings) { 4628 var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook; 4629 4630 keepUrlHook = { 4631 set: function($elm, value, name) { 4632 if (settings.url_converter) { 4633 value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]); 4634 } 4635 4636 $elm.attr('data-mce-' + name, value).attr(name, value); 4637 }, 4638 4639 get: function($elm, name) { 4640 return $elm.attr('data-mce-' + name) || $elm.attr(name); 4641 } 4642 }; 4643 4644 attrHooks = { 4645 style: { 4646 set: function($elm, value) { 4647 if (value !== null && typeof value === 'object') { 4648 $elm.css(value); 4649 return; 4650 } 4651 4652 if (keepValues) { 4653 $elm.attr('data-mce-style', value); 4654 } 4655 4656 $elm.attr('style', value); 4657 }, 4658 4659 get: function($elm) { 4660 var value = $elm.attr('data-mce-style') || $elm.attr('style'); 4661 4662 value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName); 4663 4664 return value; 4665 } 4666 } 4667 }; 4668 4669 if (keepValues) { 4670 attrHooks.href = attrHooks.src = keepUrlHook; 4671 } 4672 4673 return attrHooks; 4674 } 4675 4676 /** 4677 * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class. 4678 * 4679 * @constructor 4680 * @method DOMUtils 4681 * @param {Document} d Document reference to bind the utility class to. 4682 * @param {settings} s Optional settings collection. 4683 */ 4684 function DOMUtils(doc, settings) { 4685 var self = this, blockElementsMap; 4686 4687 self.doc = doc; 4688 self.win = window; 4689 self.files = {}; 4690 self.counter = 0; 4691 self.stdMode = !isIE || doc.documentMode >= 8; 4692 self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode; 4693 self.styleSheetLoader = new StyleSheetLoader(doc); 4694 self.boundEvents = []; 4695 self.settings = settings = settings || {}; 4696 self.schema = settings.schema; 4697 self.styles = new Styles({ 4698 url_converter: settings.url_converter, 4699 url_converter_scope: settings.url_converter_scope 4700 }, settings.schema); 4701 4702 self.fixDoc(doc); 4703 self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event; 4704 self.attrHooks = setupAttrHooks(self, settings); 4705 blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {}; 4706 self.$ = $.overrideDefaults(function() { 4707 return { 4708 context: doc, 4709 element: self.getRoot() 4710 }; 4711 }); 4712 4713 /** 4714 * Returns true/false if the specified element is a block element or not. 4715 * 4716 * @method isBlock 4717 * @param {Node/String} node Element/Node to check. 4718 * @return {Boolean} True/False state if the node is a block element or not. 4719 */ 4720 self.isBlock = function(node) { 4721 // Fix for #5446 4722 if (!node) { 4723 return false; 4724 } 4725 4726 // This function is called in module pattern style since it might be executed with the wrong this scope 4727 var type = node.nodeType; 4728 4729 // If it's a node then check the type and use the nodeName 4730 if (type) { 4731 return !!(type === 1 && blockElementsMap[node.nodeName]); 4732 } 4733 4734 return !!blockElementsMap[node]; 4735 }; 4736 } 4737 4738 DOMUtils.prototype = { 4739 $$: function(elm) { 4740 if (typeof elm == 'string') { 4741 elm = this.get(elm); 4742 } 4743 4744 return this.$(elm); 4745 }, 4746 4747 root: null, 4748 4749 fixDoc: function(doc) { 4750 var settings = this.settings, name; 4751 4752 if (isIE && settings.schema) { 4753 // Add missing HTML 4/5 elements to IE 4754 ('abbr article aside audio canvas ' + 4755 'details figcaption figure footer ' + 4756 'header hgroup mark menu meter nav ' + 4757 'output progress section summary ' + 4758 'time video').replace(/\w+/g, function(name) { 4759 doc.createElement(name); 4760 }); 4761 4762 // Create all custom elements 4763 for (name in settings.schema.getCustomElements()) { 4764 doc.createElement(name); 4765 } 4766 } 4767 }, 4768 4769 clone: function(node, deep) { 4770 var self = this, clone, doc; 4771 4772 // TODO: Add feature detection here in the future 4773 if (!isIE || node.nodeType !== 1 || deep) { 4774 return node.cloneNode(deep); 4775 } 4776 4777 doc = self.doc; 4778 4779 // Make a HTML5 safe shallow copy 4780 if (!deep) { 4781 clone = doc.createElement(node.nodeName); 4782 4783 // Copy attribs 4784 each(self.getAttribs(node), function(attr) { 4785 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); 4786 }); 4787 4788 return clone; 4789 } 4790 4791 return clone.firstChild; 4792 }, 4793 4794 /** 4795 * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not 4796 * go above the point of this root node. 4797 * 4798 * @method getRoot 4799 * @return {Element} Root element for the utility class. 4800 */ 4801 getRoot: function() { 4802 var self = this; 4803 4804 return self.settings.root_element || self.doc.body; 4805 }, 4806 4807 /** 4808 * Returns the viewport of the window. 4809 * 4810 * @method getViewPort 4811 * @param {Window} win Optional window to get viewport of. 4812 * @return {Object} Viewport object with fields x, y, w and h. 4813 */ 4814 getViewPort: function(win) { 4815 var doc, rootElm; 4816 4817 win = !win ? this.win : win; 4818 doc = win.document; 4819 rootElm = this.boxModel ? doc.documentElement : doc.body; 4820 4821 // Returns viewport size excluding scrollbars 4822 return { 4823 x: win.pageXOffset || rootElm.scrollLeft, 4824 y: win.pageYOffset || rootElm.scrollTop, 4825 w: win.innerWidth || rootElm.clientWidth, 4826 h: win.innerHeight || rootElm.clientHeight 4827 }; 4828 }, 4829 4830 /** 4831 * Returns the rectangle for a specific element. 4832 * 4833 * @method getRect 4834 * @param {Element/String} elm Element object or element ID to get rectangle from. 4835 * @return {object} Rectangle for specified element object with x, y, w, h fields. 4836 */ 4837 getRect: function(elm) { 4838 var self = this, pos, size; 4839 4840 elm = self.get(elm); 4841 pos = self.getPos(elm); 4842 size = self.getSize(elm); 4843 4844 return { 4845 x: pos.x, y: pos.y, 4846 w: size.w, h: size.h 4847 }; 4848 }, 4849 4850 /** 4851 * Returns the size dimensions of the specified element. 4852 * 4853 * @method getSize 4854 * @param {Element/String} elm Element object or element ID to get rectangle from. 4855 * @return {object} Rectangle for specified element object with w, h fields. 4856 */ 4857 getSize: function(elm) { 4858 var self = this, w, h; 4859 4860 elm = self.get(elm); 4861 w = self.getStyle(elm, 'width'); 4862 h = self.getStyle(elm, 'height'); 4863 4864 // Non pixel value, then force offset/clientWidth 4865 if (w.indexOf('px') === -1) { 4866 w = 0; 4867 } 4868 4869 // Non pixel value, then force offset/clientWidth 4870 if (h.indexOf('px') === -1) { 4871 h = 0; 4872 } 4873 4874 return { 4875 w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth, 4876 h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight 4877 }; 4878 }, 4879 4880 /** 4881 * Returns a node by the specified selector function. This function will 4882 * loop through all parent nodes and call the specified function for each node. 4883 * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end 4884 * and the node it found will be returned. 4885 * 4886 * @method getParent 4887 * @param {Node/String} node DOM node to search parents on or ID string. 4888 * @param {function} selector Selection function or CSS selector to execute on each node. 4889 * @param {Node} root Optional root element, never go below this point. 4890 * @return {Node} DOM Node or null if it wasn't found. 4891 */ 4892 getParent: function(node, selector, root) { 4893 return this.getParents(node, selector, root, false); 4894 }, 4895 4896 /** 4897 * Returns a node list of all parents matching the specified selector function or pattern. 4898 * If the function then returns true indicating that it has found what it was looking for and that node will be collected. 4899 * 4900 * @method getParents 4901 * @param {Node/String} node DOM node to search parents on or ID string. 4902 * @param {function} selector Selection function to execute on each node or CSS pattern. 4903 * @param {Node} root Optional root element, never go below this point. 4904 * @return {Array} Array of nodes or null if it wasn't found. 4905 */ 4906 getParents: function(node, selector, root, collect) { 4907 var self = this, selectorVal, result = []; 4908 4909 node = self.get(node); 4910 collect = collect === undefined; 4911 4912 // Default root on inline mode 4913 root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null); 4914 4915 // Wrap node name as func 4916 if (is(selector, 'string')) { 4917 selectorVal = selector; 4918 4919 if (selector === '*') { 4920 selector = function(node) { 4921 return node.nodeType == 1; 4922 }; 4923 } else { 4924 selector = function(node) { 4925 return self.is(node, selectorVal); 4926 }; 4927 } 4928 } 4929 4930 while (node) { 4931 if (node == root || !node.nodeType || node.nodeType === 9) { 4932 break; 4933 } 4934 4935 if (!selector || selector(node)) { 4936 if (collect) { 4937 result.push(node); 4938 } else { 4939 return node; 4940 } 4941 } 4942 4943 node = node.parentNode; 4944 } 4945 4946 return collect ? result : null; 4947 }, 4948 4949 /** 4950 * Returns the specified element by ID or the input element if it isn't a string. 4951 * 4952 * @method get 4953 * @param {String/Element} n Element id to look for or element to just pass though. 4954 * @return {Element} Element matching the specified id or null if it wasn't found. 4955 */ 4956 get: function(elm) { 4957 var name; 4958 4959 if (elm && this.doc && typeof(elm) == 'string') { 4960 name = elm; 4961 elm = this.doc.getElementById(elm); 4962 4963 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick 4964 if (elm && elm.id !== name) { 4965 return this.doc.getElementsByName(name)[1]; 4966 } 4967 } 4968 4969 return elm; 4970 }, 4971 4972 /** 4973 * Returns the next node that matches selector or function 4974 * 4975 * @method getNext 4976 * @param {Node} node Node to find siblings from. 4977 * @param {String/function} selector Selector CSS expression or function. 4978 * @return {Node} Next node item matching the selector or null if it wasn't found. 4979 */ 4980 getNext: function(node, selector) { 4981 return this._findSib(node, selector, 'nextSibling'); 4982 }, 4983 4984 /** 4985 * Returns the previous node that matches selector or function 4986 * 4987 * @method getPrev 4988 * @param {Node} node Node to find siblings from. 4989 * @param {String/function} selector Selector CSS expression or function. 4990 * @return {Node} Previous node item matching the selector or null if it wasn't found. 4991 */ 4992 getPrev: function(node, selector) { 4993 return this._findSib(node, selector, 'previousSibling'); 4994 }, 4995 4996 // #ifndef jquery 4997 4998 /** 4999 * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test". 5000 * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough 5001 * on more complex patterns. 5002 * 5003 * @method select 5004 * @param {String} selector CSS level 3 pattern to select/find elements by. 5005 * @param {Object} scope Optional root element/scope element to search in. 5006 * @return {Array} Array with all matched elements. 5007 * @example 5008 * // Adds a class to all paragraphs in the currently active editor 5009 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 5010 * 5011 * // Adds a class to all spans that have the test class in the currently active editor 5012 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') 5013 */ 5014 select: function(selector, scope) { 5015 var self = this; 5016 5017 /*eslint new-cap:0 */ 5018 return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []); 5019 }, 5020 5021 /** 5022 * Returns true/false if the specified element matches the specified css pattern. 5023 * 5024 * @method is 5025 * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. 5026 * @param {String} selector CSS pattern to match the element against. 5027 */ 5028 is: function(elm, selector) { 5029 var i; 5030 5031 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance 5032 if (elm.length === undefined) { 5033 // Simple all selector 5034 if (selector === '*') { 5035 return elm.nodeType == 1; 5036 } 5037 5038 // Simple selector just elements 5039 if (simpleSelectorRe.test(selector)) { 5040 selector = selector.toLowerCase().split(/,/); 5041 elm = elm.nodeName.toLowerCase(); 5042 5043 for (i = selector.length - 1; i >= 0; i--) { 5044 if (selector[i] == elm) { 5045 return true; 5046 } 5047 } 5048 5049 return false; 5050 } 5051 } 5052 5053 // Is non element 5054 if (elm.nodeType && elm.nodeType != 1) { 5055 return false; 5056 } 5057 5058 var elms = elm.nodeType ? [elm] : elm; 5059 5060 /*eslint new-cap:0 */ 5061 return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; 5062 }, 5063 5064 // #endif 5065 5066 /** 5067 * Adds the specified element to another element or elements. 5068 * 5069 * @method add 5070 * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. 5071 * @param {String/Element} name Name of new element to add or existing element to add. 5072 * @param {Object} attrs Optional object collection with arguments to add to the new element(s). 5073 * @param {String} html Optional inner HTML contents to add for each element. 5074 * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements 5075 * were passed in. 5076 * @example 5077 * // Adds a new paragraph to the end of the active editor 5078 * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content'); 5079 */ 5080 add: function(parentElm, name, attrs, html, create) { 5081 var self = this; 5082 5083 return this.run(parentElm, function(parentElm) { 5084 var newElm; 5085 5086 newElm = is(name, 'string') ? self.doc.createElement(name) : name; 5087 self.setAttribs(newElm, attrs); 5088 5089 if (html) { 5090 if (html.nodeType) { 5091 newElm.appendChild(html); 5092 } else { 5093 self.setHTML(newElm, html); 5094 } 5095 } 5096 5097 return !create ? parentElm.appendChild(newElm) : newElm; 5098 }); 5099 }, 5100 5101 /** 5102 * Creates a new element. 5103 * 5104 * @method create 5105 * @param {String} name Name of new element. 5106 * @param {Object} attrs Optional object name/value collection with element attributes. 5107 * @param {String} html Optional HTML string to set as inner HTML of the element. 5108 * @return {Element} HTML DOM node element that got created. 5109 * @example 5110 * // Adds an element where the caret/selection is in the active editor 5111 * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content'); 5112 * tinymce.activeEditor.selection.setNode(el); 5113 */ 5114 create: function(name, attrs, html) { 5115 return this.add(this.doc.createElement(name), name, attrs, html, 1); 5116 }, 5117 5118 /** 5119 * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. 5120 * 5121 * @method createHTML 5122 * @param {String} name Name of new element. 5123 * @param {Object} attrs Optional object name/value collection with element attributes. 5124 * @param {String} html Optional HTML string to set as inner HTML of the element. 5125 * @return {String} String with new HTML element, for example: <a href="#">test</a>. 5126 * @example 5127 * // Creates a html chunk and inserts it at the current selection/caret location 5128 * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line')); 5129 */ 5130 createHTML: function(name, attrs, html) { 5131 var outHtml = '', key; 5132 5133 outHtml += '<' + name; 5134 5135 for (key in attrs) { 5136 if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') { 5137 outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"'; 5138 } 5139 } 5140 5141 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime 5142 if (typeof(html) != "undefined") { 5143 return outHtml + '>' + html + '</' + name + '>'; 5144 } 5145 5146 return outHtml + ' />'; 5147 }, 5148 5149 /** 5150 * Creates a document fragment out of the specified HTML string. 5151 * 5152 * @method createFragment 5153 * @param {String} html Html string to create fragment from. 5154 * @return {DocumentFragment} Document fragment node. 5155 */ 5156 createFragment: function(html) { 5157 var frag, node, doc = this.doc, container; 5158 5159 container = doc.createElement("div"); 5160 frag = doc.createDocumentFragment(); 5161 5162 if (html) { 5163 container.innerHTML = html; 5164 } 5165 5166 while ((node = container.firstChild)) { 5167 frag.appendChild(node); 5168 } 5169 5170 return frag; 5171 }, 5172 5173 /** 5174 * Removes/deletes the specified element(s) from the DOM. 5175 * 5176 * @method remove 5177 * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. 5178 * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be 5179 * placed at the location of the removed element. 5180 * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements 5181 * were passed in. 5182 * @example 5183 * // Removes all paragraphs in the active editor 5184 * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); 5185 * 5186 * // Removes an element by id in the document 5187 * tinymce.DOM.remove('mydiv'); 5188 */ 5189 remove: function(node, keepChildren) { 5190 node = this.$$(node); 5191 5192 if (keepChildren) { 5193 node.each(function() { 5194 var child; 5195 5196 while ((child = this.firstChild)) { 5197 if (child.nodeType == 3 && child.data.length === 0) { 5198 this.removeChild(child); 5199 } else { 5200 this.parentNode.insertBefore(child, this); 5201 } 5202 } 5203 }).remove(); 5204 } else { 5205 node.remove(); 5206 } 5207 5208 return node.length > 1 ? node.toArray() : node[0]; 5209 }, 5210 5211 /** 5212 * Sets the CSS style value on a HTML element. The name can be a camelcase string 5213 * or the CSS style name like background-color. 5214 * 5215 * @method setStyle 5216 * @param {String/Element/Array} n HTML element/Element ID or Array of elements/ids to set CSS style value on. 5217 * @param {String} na Name of the style value to set. 5218 * @param {String} v Value to set on the style. 5219 * @example 5220 * // Sets a style value on all paragraphs in the currently active editor 5221 * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); 5222 * 5223 * // Sets a style value to an element by id in the current document 5224 * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); 5225 */ 5226 setStyle: function(elm, name, value) { 5227 elm = this.$$(elm).css(name, value); 5228 5229 if (this.settings.update_styles) { 5230 elm.attr('data-mce-style', null); 5231 } 5232 }, 5233 5234 /** 5235 * Returns the current style or runtime/computed value of an element. 5236 * 5237 * @method getStyle 5238 * @param {String/Element} elm HTML element or element id string to get style from. 5239 * @param {String} name Style name to return. 5240 * @param {Boolean} computed Computed style. 5241 * @return {String} Current style or computed style value of an element. 5242 */ 5243 getStyle: function(elm, name, computed) { 5244 elm = this.$$(elm); 5245 5246 if (computed) { 5247 return elm.css(name); 5248 } 5249 5250 // Camelcase it, if needed 5251 name = name.replace(/-(\D)/g, function(a, b) { 5252 return b.toUpperCase(); 5253 }); 5254 5255 if (name == 'float') { 5256 name = isIE ? 'styleFloat' : 'cssFloat'; 5257 } 5258 5259 return elm[0] && elm[0].style ? elm[0].style[name] : undefined; 5260 }, 5261 5262 /** 5263 * Sets multiple styles on the specified element(s). 5264 * 5265 * @method setStyles 5266 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set styles on. 5267 * @param {Object} o Name/Value collection of style items to add to the element(s). 5268 * @example 5269 * // Sets styles on all paragraphs in the currently active editor 5270 * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'}); 5271 * 5272 * // Sets styles to an element by id in the current document 5273 * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'}); 5274 */ 5275 setStyles: function(elm, styles) { 5276 elm = this.$$(elm).css(styles); 5277 5278 if (this.settings.update_styles) { 5279 elm.attr('data-mce-style', null); 5280 } 5281 }, 5282 5283 /** 5284 * Removes all attributes from an element or elements. 5285 * 5286 * @method removeAllAttribs 5287 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. 5288 */ 5289 removeAllAttribs: function(e) { 5290 return this.run(e, function(e) { 5291 var i, attrs = e.attributes; 5292 for (i = attrs.length - 1; i >= 0; i--) { 5293 e.removeAttributeNode(attrs.item(i)); 5294 } 5295 }); 5296 }, 5297 5298 /** 5299 * Sets the specified attribute of an element or elements. 5300 * 5301 * @method setAttrib 5302 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set attribute on. 5303 * @param {String} n Name of attribute to set. 5304 * @param {String} v Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove the attribute instead. 5305 * @example 5306 * // Sets class attribute on all paragraphs in the active editor 5307 * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); 5308 * 5309 * // Sets class attribute on a specific element in the current page 5310 * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); 5311 */ 5312 setAttrib: function(elm, name, value) { 5313 var self = this, originalValue, hook, settings = self.settings; 5314 5315 if (value === '') { 5316 value = null; 5317 } 5318 5319 elm = self.$$(elm); 5320 originalValue = elm.attr(name); 5321 5322 if (!elm.length) { 5323 return; 5324 } 5325 5326 hook = self.attrHooks[name]; 5327 if (hook && hook.set) { 5328 hook.set(elm, value, name); 5329 } else { 5330 elm.attr(name, value); 5331 } 5332 5333 if (originalValue != value && settings.onSetAttrib) { 5334 settings.onSetAttrib({ 5335 attrElm: elm, 5336 attrName: name, 5337 attrValue: value 5338 }); 5339 } 5340 }, 5341 5342 /** 5343 * Sets two or more specified attributes of an element or elements. 5344 * 5345 * @method setAttribs 5346 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. 5347 * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). 5348 * @example 5349 * // Sets class and title attributes on all paragraphs in the active editor 5350 * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'}); 5351 * 5352 * // Sets class and title attributes on a specific element in the current page 5353 * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'}); 5354 */ 5355 setAttribs: function(elm, attrs) { 5356 var self = this; 5357 5358 self.$$(elm).each(function(i, node) { 5359 each(attrs, function(value, name) { 5360 self.setAttrib(node, name, value); 5361 }); 5362 }); 5363 }, 5364 5365 /** 5366 * Returns the specified attribute by name. 5367 * 5368 * @method getAttrib 5369 * @param {String/Element} elm Element string id or DOM element to get attribute from. 5370 * @param {String} name Name of attribute to get. 5371 * @param {String} defaultVal Optional default value to return if the attribute didn't exist. 5372 * @return {String} Attribute value string, default value or null if the attribute wasn't found. 5373 */ 5374 getAttrib: function(elm, name, defaultVal) { 5375 var self = this, hook, value; 5376 5377 elm = self.$$(elm); 5378 5379 if (elm.length) { 5380 hook = self.attrHooks[name]; 5381 5382 if (hook && hook.get) { 5383 value = hook.get(elm, name); 5384 } else { 5385 value = elm.attr(name); 5386 } 5387 } 5388 5389 if (typeof value == 'undefined') { 5390 value = defaultVal || ''; 5391 } 5392 5393 return value; 5394 }, 5395 5396 /** 5397 * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. 5398 * 5399 * @method getPos 5400 * @param {Element/String} elm HTML element or element id to get x, y position from. 5401 * @param {Element} rootElm Optional root element to stop calculations at. 5402 * @return {object} Absolute position of the specified element object with x, y fields. 5403 */ 5404 getPos: function(elm, rootElm) { 5405 var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos; 5406 5407 elm = self.get(elm); 5408 rootElm = rootElm || body; 5409 5410 if (elm) { 5411 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 5412 // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root 5413 if (rootElm === body && elm.getBoundingClientRect && $(body).css('position') === 'static') { 5414 pos = elm.getBoundingClientRect(); 5415 rootElm = self.boxModel ? doc.documentElement : body; 5416 5417 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 5418 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 5419 x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft; 5420 y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop; 5421 5422 return {x: x, y: y}; 5423 } 5424 5425 offsetParent = elm; 5426 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { 5427 x += offsetParent.offsetLeft || 0; 5428 y += offsetParent.offsetTop || 0; 5429 offsetParent = offsetParent.offsetParent; 5430 } 5431 5432 offsetParent = elm.parentNode; 5433 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { 5434 x -= offsetParent.scrollLeft || 0; 5435 y -= offsetParent.scrollTop || 0; 5436 offsetParent = offsetParent.parentNode; 5437 } 5438 } 5439 5440 return {x: x, y: y}; 5441 }, 5442 5443 /** 5444 * Parses the specified style value into an object collection. This parser will also 5445 * merge and remove any redundant items that browsers might have added. It will also convert non-hex 5446 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 5447 * 5448 * @method parseStyle 5449 * @param {String} cssText Style value to parse, for example: border:1px solid red;. 5450 * @return {Object} Object representation of that style, for example: {border: '1px solid red'} 5451 */ 5452 parseStyle: function(cssText) { 5453 return this.styles.parse(cssText); 5454 }, 5455 5456 /** 5457 * Serializes the specified style object into a string. 5458 * 5459 * @method serializeStyle 5460 * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} 5461 * @param {String} name Optional element name. 5462 * @return {String} String representation of the style object, for example: border: 1px solid red. 5463 */ 5464 serializeStyle: function(styles, name) { 5465 return this.styles.serialize(styles, name); 5466 }, 5467 5468 /** 5469 * Adds a style element at the top of the document with the specified cssText content. 5470 * 5471 * @method addStyle 5472 * @param {String} cssText CSS Text style to add to top of head of document. 5473 */ 5474 addStyle: function(cssText) { 5475 var self = this, doc = self.doc, head, styleElm; 5476 5477 // Prevent inline from loading the same styles twice 5478 if (self !== DOMUtils.DOM && doc === document) { 5479 var addedStyles = DOMUtils.DOM.addedStyles; 5480 5481 addedStyles = addedStyles || []; 5482 if (addedStyles[cssText]) { 5483 return; 5484 } 5485 5486 addedStyles[cssText] = true; 5487 DOMUtils.DOM.addedStyles = addedStyles; 5488 } 5489 5490 // Create style element if needed 5491 styleElm = doc.getElementById('mceDefaultStyles'); 5492 if (!styleElm) { 5493 styleElm = doc.createElement('style'); 5494 styleElm.id = 'mceDefaultStyles'; 5495 styleElm.type = 'text/css'; 5496 5497 head = doc.getElementsByTagName('head')[0]; 5498 if (head.firstChild) { 5499 head.insertBefore(styleElm, head.firstChild); 5500 } else { 5501 head.appendChild(styleElm); 5502 } 5503 } 5504 5505 // Append style data to old or new style element 5506 if (styleElm.styleSheet) { 5507 styleElm.styleSheet.cssText += cssText; 5508 } else { 5509 styleElm.appendChild(doc.createTextNode(cssText)); 5510 } 5511 }, 5512 5513 /** 5514 * Imports/loads the specified CSS file into the document bound to the class. 5515 * 5516 * @method loadCSS 5517 * @param {String} u URL to CSS file to load. 5518 * @example 5519 * // Loads a CSS file dynamically into the current document 5520 * tinymce.DOM.loadCSS('somepath/some.css'); 5521 * 5522 * // Loads a CSS file into the currently active editor instance 5523 * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); 5524 * 5525 * // Loads a CSS file into an editor instance by id 5526 * tinymce.get('someid').dom.loadCSS('somepath/some.css'); 5527 * 5528 * // Loads multiple CSS files into the current document 5529 * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); 5530 */ 5531 loadCSS: function(url) { 5532 var self = this, doc = self.doc, head; 5533 5534 // Prevent inline from loading the same CSS file twice 5535 if (self !== DOMUtils.DOM && doc === document) { 5536 DOMUtils.DOM.loadCSS(url); 5537 return; 5538 } 5539 5540 if (!url) { 5541 url = ''; 5542 } 5543 5544 head = doc.getElementsByTagName('head')[0]; 5545 5546 each(url.split(','), function(url) { 5547 var link; 5548 5549 if (self.files[url]) { 5550 return; 5551 } 5552 5553 self.files[url] = true; 5554 link = self.create('link', {rel: 'stylesheet', href: url}); 5555 5556 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug 5557 // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading 5558 // It's ugly but it seems to work fine. 5559 if (isIE && doc.documentMode && doc.recalc) { 5560 link.onload = function() { 5561 if (doc.recalc) { 5562 doc.recalc(); 5563 } 5564 5565 link.onload = null; 5566 }; 5567 } 5568 5569 head.appendChild(link); 5570 }); 5571 }, 5572 5573 /** 5574 * Adds a class to the specified element or elements. 5575 * 5576 * @method addClass 5577 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 5578 * @param {String} cls Class name to add to each element. 5579 * @return {String/Array} String with new class value or array with new class values for all elements. 5580 * @example 5581 * // Adds a class to all paragraphs in the active editor 5582 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 5583 * 5584 * // Adds a class to a specific element in the current page 5585 * tinymce.DOM.addClass('mydiv', 'myclass'); 5586 */ 5587 addClass: function(elm, cls) { 5588 this.$$(elm).addClass(cls); 5589 }, 5590 5591 /** 5592 * Removes a class from the specified element or elements. 5593 * 5594 * @method removeClass 5595 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 5596 * @param {String} cls Class name to remove from each element. 5597 * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements 5598 * were passed in. 5599 * @example 5600 * // Removes a class from all paragraphs in the active editor 5601 * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 5602 * 5603 * // Removes a class from a specific element in the current page 5604 * tinymce.DOM.removeClass('mydiv', 'myclass'); 5605 */ 5606 removeClass: function(elm, cls) { 5607 this.toggleClass(elm, cls, false); 5608 }, 5609 5610 /** 5611 * Returns true if the specified element has the specified class. 5612 * 5613 * @method hasClass 5614 * @param {String/Element} n HTML element or element id string to check CSS class on. 5615 * @param {String} c CSS class to check for. 5616 * @return {Boolean} true/false if the specified element has the specified class. 5617 */ 5618 hasClass: function(elm, cls) { 5619 return this.$$(elm).hasClass(cls); 5620 }, 5621 5622 /** 5623 * Toggles the specified class on/off. 5624 * 5625 * @method toggleClass 5626 * @param {Element} elm Element to toggle class on. 5627 * @param {[type]} cls Class to toggle on/off. 5628 * @param {[type]} state Optional state to set. 5629 */ 5630 toggleClass: function(elm, cls, state) { 5631 this.$$(elm).toggleClass(cls, state).each(function() { 5632 if (this.className === '') { 5633 $(this).attr('class', null); 5634 } 5635 }); 5636 }, 5637 5638 /** 5639 * Shows the specified element(s) by ID by setting the "display" style. 5640 * 5641 * @method show 5642 * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. 5643 */ 5644 show: function(elm) { 5645 this.$$(elm).show(); 5646 }, 5647 5648 /** 5649 * Hides the specified element(s) by ID by setting the "display" style. 5650 * 5651 * @method hide 5652 * @param {String/Element/Array} e ID of DOM element or DOM element or array with elements or IDs to hide. 5653 * @example 5654 * // Hides an element by id in the document 5655 * tinymce.DOM.hide('myid'); 5656 */ 5657 hide: function(elm) { 5658 this.$$(elm).hide(); 5659 }, 5660 5661 /** 5662 * Returns true/false if the element is hidden or not by checking the "display" style. 5663 * 5664 * @method isHidden 5665 * @param {String/Element} e Id or element to check display state on. 5666 * @return {Boolean} true/false if the element is hidden or not. 5667 */ 5668 isHidden: function(elm) { 5669 return this.$$(elm).css('display') == 'none'; 5670 }, 5671 5672 /** 5673 * Returns a unique id. This can be useful when generating elements on the fly. 5674 * This method will not check if the element already exists. 5675 * 5676 * @method uniqueId 5677 * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". 5678 * @return {String} Unique id. 5679 */ 5680 uniqueId: function(prefix) { 5681 return (!prefix ? 'mce_' : prefix) + (this.counter++); 5682 }, 5683 5684 /** 5685 * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means 5686 * URLs will get converted, hex color values fixed etc. Check processHTML for details. 5687 * 5688 * @method setHTML 5689 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of. 5690 * @param {String} h HTML content to set as inner HTML of the element. 5691 * @example 5692 * // Sets the inner HTML of all paragraphs in the active editor 5693 * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); 5694 * 5695 * // Sets the inner HTML of an element by id in the document 5696 * tinymce.DOM.setHTML('mydiv', 'some inner html'); 5697 */ 5698 setHTML: function(elm, html) { 5699 elm = this.$$(elm); 5700 5701 if (isIE) { 5702 elm.each(function(i, target) { 5703 if (target.canHaveHTML === false) { 5704 return; 5705 } 5706 5707 // Remove all child nodes, IE keeps empty text nodes in DOM 5708 while (target.firstChild) { 5709 target.removeChild(target.firstChild); 5710 } 5711 5712 try { 5713 // IE will remove comments from the beginning 5714 // unless you padd the contents with something 5715 target.innerHTML = '<br>' + html; 5716 target.removeChild(target.firstChild); 5717 } catch (ex) { 5718 // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p 5719 $('<div>').html('<br>' + html).contents().slice(1).appendTo(target); 5720 } 5721 5722 return html; 5723 }); 5724 } else { 5725 elm.html(html); 5726 } 5727 }, 5728 5729 /** 5730 * Returns the outer HTML of an element. 5731 * 5732 * @method getOuterHTML 5733 * @param {String/Element} elm Element ID or element object to get outer HTML from. 5734 * @return {String} Outer HTML string. 5735 * @example 5736 * tinymce.DOM.getOuterHTML(editorElement); 5737 * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); 5738 */ 5739 getOuterHTML: function(elm) { 5740 elm = this.get(elm); 5741 return elm.nodeType == 1 ? elm.outerHTML : $('<div>').append($(elm).clone()).html(); 5742 }, 5743 5744 /** 5745 * Sets the specified outer HTML on an element or elements. 5746 * 5747 * @method setOuterHTML 5748 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. 5749 * @param {Object} html HTML code to set as outer value for the element. 5750 * @param {Document} doc Optional document scope to use in this process - defaults to the document of the DOM class. 5751 * @example 5752 * // Sets the outer HTML of all paragraphs in the active editor 5753 * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>'); 5754 * 5755 * // Sets the outer HTML of an element by id in the document 5756 * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>'); 5757 */ 5758 setOuterHTML: function(elm, html) { 5759 var self = this; 5760 5761 self.$$(elm).each(function() { 5762 try { 5763 this.outerHTML = html; 5764 } catch (ex) { 5765 // OuterHTML for IE it sometimes produces an "unknown runtime error" 5766 self.remove($(this).html(html), true); 5767 } 5768 }); 5769 }, 5770 5771 /** 5772 * Entity decodes a string. This method decodes any HTML entities, such as å. 5773 * 5774 * @method decode 5775 * @param {String} s String to decode entities on. 5776 * @return {String} Entity decoded string. 5777 */ 5778 decode: Entities.decode, 5779 5780 /** 5781 * Entity encodes a string. This method encodes the most common entities, such as <>"&. 5782 * 5783 * @method encode 5784 * @param {String} text String to encode with entities. 5785 * @return {String} Entity encoded string. 5786 */ 5787 encode: Entities.encodeAllRaw, 5788 5789 /** 5790 * Inserts an element after the reference element. 5791 * 5792 * @method insertAfter 5793 * @param {Element} node Element to insert after the reference. 5794 * @param {Element/String/Array} reference_node Reference element, element id or array of elements to insert after. 5795 * @return {Element/Array} Element that got added or an array with elements. 5796 */ 5797 insertAfter: function(node, referenceNode) { 5798 referenceNode = this.get(referenceNode); 5799 5800 return this.run(node, function(node) { 5801 var parent, nextSibling; 5802 5803 parent = referenceNode.parentNode; 5804 nextSibling = referenceNode.nextSibling; 5805 5806 if (nextSibling) { 5807 parent.insertBefore(node, nextSibling); 5808 } else { 5809 parent.appendChild(node); 5810 } 5811 5812 return node; 5813 }); 5814 }, 5815 5816 /** 5817 * Replaces the specified element or elements with the new element specified. The new element will 5818 * be cloned if multiple input elements are passed in. 5819 * 5820 * @method replace 5821 * @param {Element} newElm New element to replace old ones with. 5822 * @param {Element/String/Array} oldELm Element DOM node, element id or array of elements or ids to replace. 5823 * @param {Boolean} k Optional keep children state, if set to true child nodes from the old object will be added to new ones. 5824 */ 5825 replace: function(newElm, oldElm, keepChildren) { 5826 var self = this; 5827 5828 return self.run(oldElm, function(oldElm) { 5829 if (is(oldElm, 'array')) { 5830 newElm = newElm.cloneNode(true); 5831 } 5832 5833 if (keepChildren) { 5834 each(grep(oldElm.childNodes), function(node) { 5835 newElm.appendChild(node); 5836 }); 5837 } 5838 5839 return oldElm.parentNode.replaceChild(newElm, oldElm); 5840 }); 5841 }, 5842 5843 /** 5844 * Renames the specified element and keeps its attributes and children. 5845 * 5846 * @method rename 5847 * @param {Element} elm Element to rename. 5848 * @param {String} name Name of the new element. 5849 * @return {Element} New element or the old element if it needed renaming. 5850 */ 5851 rename: function(elm, name) { 5852 var self = this, newElm; 5853 5854 if (elm.nodeName != name.toUpperCase()) { 5855 // Rename block element 5856 newElm = self.create(name); 5857 5858 // Copy attribs to new block 5859 each(self.getAttribs(elm), function(attrNode) { 5860 self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName)); 5861 }); 5862 5863 // Replace block 5864 self.replace(newElm, elm, 1); 5865 } 5866 5867 return newElm || elm; 5868 }, 5869 5870 /** 5871 * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. 5872 * 5873 * @method findCommonAncestor 5874 * @param {Element} a Element to find common ancestor of. 5875 * @param {Element} b Element to find common ancestor of. 5876 * @return {Element} Common ancestor element of the two input elements. 5877 */ 5878 findCommonAncestor: function(a, b) { 5879 var ps = a, pe; 5880 5881 while (ps) { 5882 pe = b; 5883 5884 while (pe && ps != pe) { 5885 pe = pe.parentNode; 5886 } 5887 5888 if (ps == pe) { 5889 break; 5890 } 5891 5892 ps = ps.parentNode; 5893 } 5894 5895 if (!ps && a.ownerDocument) { 5896 return a.ownerDocument.documentElement; 5897 } 5898 5899 return ps; 5900 }, 5901 5902 /** 5903 * Parses the specified RGB color value and returns a hex version of that color. 5904 * 5905 * @method toHex 5906 * @param {String} rgbVal RGB string value like rgb(1,2,3) 5907 * @return {String} Hex version of that RGB value like #FF00FF. 5908 */ 5909 toHex: function(rgbVal) { 5910 return this.styles.toHex(Tools.trim(rgbVal)); 5911 }, 5912 5913 /** 5914 * Executes the specified function on the element by id or dom element node or array of elements/id. 5915 * 5916 * @method run 5917 * @param {String/Element/Array} Element ID or DOM element object or array with ids or elements. 5918 * @param {function} f Function to execute for each item. 5919 * @param {Object} s Optional scope to execute the function in. 5920 * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. 5921 */ 5922 run: function(elm, func, scope) { 5923 var self = this, result; 5924 5925 if (typeof(elm) === 'string') { 5926 elm = self.get(elm); 5927 } 5928 5929 if (!elm) { 5930 return false; 5931 } 5932 5933 scope = scope || this; 5934 if (!elm.nodeType && (elm.length || elm.length === 0)) { 5935 result = []; 5936 5937 each(elm, function(elm, i) { 5938 if (elm) { 5939 if (typeof(elm) == 'string') { 5940 elm = self.get(elm); 5941 } 5942 5943 result.push(func.call(scope, elm, i)); 5944 } 5945 }); 5946 5947 return result; 5948 } 5949 5950 return func.call(scope, elm); 5951 }, 5952 5953 /** 5954 * Returns a NodeList with attributes for the element. 5955 * 5956 * @method getAttribs 5957 * @param {HTMLElement/string} elm Element node or string id to get attributes from. 5958 * @return {NodeList} NodeList with attributes. 5959 */ 5960 getAttribs: function(elm) { 5961 var attrs; 5962 5963 elm = this.get(elm); 5964 5965 if (!elm) { 5966 return []; 5967 } 5968 5969 if (isIE) { 5970 attrs = []; 5971 5972 // Object will throw exception in IE 5973 if (elm.nodeName == 'OBJECT') { 5974 return elm.attributes; 5975 } 5976 5977 // IE doesn't keep the selected attribute if you clone option elements 5978 if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) { 5979 attrs.push({specified: 1, nodeName: 'selected'}); 5980 } 5981 5982 // It's crazy that this is faster in IE but it's because it returns all attributes all the time 5983 var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi; 5984 elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function(a) { 5985 attrs.push({specified: 1, nodeName: a}); 5986 }); 5987 5988 return attrs; 5989 } 5990 5991 return elm.attributes; 5992 }, 5993 5994 /** 5995 * Returns true/false if the specified node is to be considered empty or not. 5996 * 5997 * @example 5998 * tinymce.DOM.isEmpty(node, {img: true}); 5999 * @method isEmpty 6000 * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. 6001 * @return {Boolean} true/false if the node is empty or not. 6002 */ 6003 isEmpty: function(node, elements) { 6004 var self = this, i, attributes, type, walker, name, brCount = 0; 6005 6006 node = node.firstChild; 6007 if (node) { 6008 walker = new TreeWalker(node, node.parentNode); 6009 elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null); 6010 6011 do { 6012 type = node.nodeType; 6013 6014 if (type === 1) { 6015 // Ignore bogus elements 6016 if (node.getAttribute('data-mce-bogus')) { 6017 continue; 6018 } 6019 6020 // Keep empty elements like <img /> 6021 name = node.nodeName.toLowerCase(); 6022 if (elements && elements[name]) { 6023 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> 6024 if (name === 'br') { 6025 brCount++; 6026 continue; 6027 } 6028 6029 return false; 6030 } 6031 6032 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> 6033 attributes = self.getAttribs(node); 6034 i = attributes.length; 6035 while (i--) { 6036 name = attributes[i].nodeName; 6037 if (name === "name" || name === 'data-mce-bookmark') { 6038 return false; 6039 } 6040 } 6041 } 6042 6043 // Keep comment nodes 6044 if (type == 8) { 6045 return false; 6046 } 6047 6048 // Keep non whitespace text nodes 6049 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) { 6050 return false; 6051 } 6052 } while ((node = walker.next())); 6053 } 6054 6055 return brCount <= 1; 6056 }, 6057 6058 /** 6059 * Creates a new DOM Range object. This will use the native DOM Range API if it's 6060 * available. If it's not, it will fall back to the custom TinyMCE implementation. 6061 * 6062 * @method createRng 6063 * @return {DOMRange} DOM Range object. 6064 * @example 6065 * var rng = tinymce.DOM.createRng(); 6066 * alert(rng.startContainer + "," + rng.startOffset); 6067 */ 6068 createRng: function() { 6069 var doc = this.doc; 6070 6071 return doc.createRange ? doc.createRange() : new Range(this); 6072 }, 6073 6074 /** 6075 * Returns the index of the specified node within its parent. 6076 * 6077 * @method nodeIndex 6078 * @param {Node} node Node to look for. 6079 * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization. 6080 * @return {Number} Index of the specified node. 6081 */ 6082 nodeIndex: function(node, normalized) { 6083 var idx = 0, lastNodeType, nodeType; 6084 6085 if (node) { 6086 for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) { 6087 nodeType = node.nodeType; 6088 6089 // Normalize text nodes 6090 if (normalized && nodeType == 3) { 6091 if (nodeType == lastNodeType || !node.nodeValue.length) { 6092 continue; 6093 } 6094 } 6095 idx++; 6096 lastNodeType = nodeType; 6097 } 6098 } 6099 6100 return idx; 6101 }, 6102 6103 /** 6104 * Splits an element into two new elements and places the specified split 6105 * element or elements between the new ones. For example splitting the paragraph at the bold element in 6106 * this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>. 6107 * 6108 * @method split 6109 * @param {Element} parentElm Parent element to split. 6110 * @param {Element} splitElm Element to split at. 6111 * @param {Element} replacementElm Optional replacement element to replace the split element with. 6112 * @return {Element} Returns the split element or the replacement element if that is specified. 6113 */ 6114 split: function(parentElm, splitElm, replacementElm) { 6115 var self = this, r = self.createRng(), bef, aft, pa; 6116 6117 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense 6118 // but we don't want that in our code since it serves no purpose for the end user 6119 // For example splitting this html at the bold element: 6120 // <p>text 1<span><b>CHOP</b></span>text 2</p> 6121 // would produce: 6122 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 6123 // this function will then trim off empty edges and produce: 6124 // <p>text 1</p><b>CHOP</b><p>text 2</p> 6125 function trimNode(node) { 6126 var i, children = node.childNodes, type = node.nodeType; 6127 6128 function surroundedBySpans(node) { 6129 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; 6130 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; 6131 return previousIsSpan && nextIsSpan; 6132 } 6133 6134 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { 6135 return; 6136 } 6137 6138 for (i = children.length - 1; i >= 0; i--) { 6139 trimNode(children[i]); 6140 } 6141 6142 if (type != 9) { 6143 // Keep non whitespace text nodes 6144 if (type == 3 && node.nodeValue.length > 0) { 6145 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" 6146 // Also keep text nodes with only spaces if surrounded by spans. 6147 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 6148 var trimmedLength = trim(node.nodeValue).length; 6149 if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { 6150 return; 6151 } 6152 } else if (type == 1) { 6153 // If the only child is a bookmark then move it up 6154 children = node.childNodes; 6155 6156 // TODO fix this complex if 6157 if (children.length == 1 && children[0] && children[0].nodeType == 1 && 6158 children[0].getAttribute('data-mce-type') == 'bookmark') { 6159 node.parentNode.insertBefore(children[0], node); 6160 } 6161 6162 // Keep non empty elements or img, hr etc 6163 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { 6164 return; 6165 } 6166 } 6167 6168 self.remove(node); 6169 } 6170 6171 return node; 6172 } 6173 6174 if (parentElm && splitElm) { 6175 // Get before chunk 6176 r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); 6177 r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); 6178 bef = r.extractContents(); 6179 6180 // Get after chunk 6181 r = self.createRng(); 6182 r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); 6183 r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); 6184 aft = r.extractContents(); 6185 6186 // Insert before chunk 6187 pa = parentElm.parentNode; 6188 pa.insertBefore(trimNode(bef), parentElm); 6189 6190 // Insert middle chunk 6191 if (replacementElm) { 6192 pa.replaceChild(replacementElm, splitElm); 6193 } else { 6194 pa.insertBefore(splitElm, parentElm); 6195 } 6196 6197 // Insert after chunk 6198 pa.insertBefore(trimNode(aft), parentElm); 6199 self.remove(parentElm); 6200 6201 return replacementElm || splitElm; 6202 } 6203 }, 6204 6205 /** 6206 * Adds an event handler to the specified object. 6207 * 6208 * @method bind 6209 * @param {Element/Document/Window/Array} target Target element to bind events to. 6210 * handler to or an array of elements/ids/documents. 6211 * @param {String} name Name of event handler to add, for example: click. 6212 * @param {function} func Function to execute when the event occurs. 6213 * @param {Object} scope Optional scope to execute the function in. 6214 * @return {function} Function callback handler the same as the one passed in. 6215 */ 6216 bind: function(target, name, func, scope) { 6217 var self = this; 6218 6219 if (Tools.isArray(target)) { 6220 var i = target.length; 6221 6222 while (i--) { 6223 target[i] = self.bind(target[i], name, func, scope); 6224 } 6225 6226 return target; 6227 } 6228 6229 // Collect all window/document events bound by editor instance 6230 if (self.settings.collect && (target === self.doc || target === self.win)) { 6231 self.boundEvents.push([target, name, func, scope]); 6232 } 6233 6234 return self.events.bind(target, name, func, scope || self); 6235 }, 6236 6237 /** 6238 * Removes the specified event handler by name and function from an element or collection of elements. 6239 * 6240 * @method unbind 6241 * @param {Element/Document/Window/Array} target Target element to unbind events on. 6242 * @param {String} name Event handler name, for example: "click" 6243 * @param {function} func Function to remove. 6244 * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements 6245 * were passed in. 6246 */ 6247 unbind: function(target, name, func) { 6248 var self = this, i; 6249 6250 if (Tools.isArray(target)) { 6251 i = target.length; 6252 6253 while (i--) { 6254 target[i] = self.unbind(target[i], name, func); 6255 } 6256 6257 return target; 6258 } 6259 6260 // Remove any bound events matching the input 6261 if (self.boundEvents && (target === self.doc || target === self.win)) { 6262 i = self.boundEvents.length; 6263 6264 while (i--) { 6265 var item = self.boundEvents[i]; 6266 6267 if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { 6268 this.events.unbind(item[0], item[1], item[2]); 6269 } 6270 } 6271 } 6272 6273 return this.events.unbind(target, name, func); 6274 }, 6275 6276 /** 6277 * Fires the specified event name with object on target. 6278 * 6279 * @method fire 6280 * @param {Node/Document/Window} target Target element or object to fire event on. 6281 * @param {String} name Name of the event to fire. 6282 * @param {Object} evt Event object to send. 6283 * @return {Event} Event object. 6284 */ 6285 fire: function(target, name, evt) { 6286 return this.events.fire(target, name, evt); 6287 }, 6288 6289 // Returns the content editable state of a node 6290 getContentEditable: function(node) { 6291 var contentEditable; 6292 6293 // Check type 6294 if (!node || node.nodeType != 1) { 6295 return null; 6296 } 6297 6298 // Check for fake content editable 6299 contentEditable = node.getAttribute("data-mce-contenteditable"); 6300 if (contentEditable && contentEditable !== "inherit") { 6301 return contentEditable; 6302 } 6303 6304 // Check for real content editable 6305 return node.contentEditable !== "inherit" ? node.contentEditable : null; 6306 }, 6307 6308 getContentEditableParent: function(node) { 6309 var root = this.getRoot(), state = null; 6310 6311 for (; node && node !== root; node = node.parentNode) { 6312 state = this.getContentEditable(node); 6313 6314 if (state !== null) { 6315 break; 6316 } 6317 } 6318 6319 return state; 6320 }, 6321 6322 /** 6323 * Destroys all internal references to the DOM to solve IE leak issues. 6324 * 6325 * @method destroy 6326 */ 6327 destroy: function() { 6328 var self = this; 6329 6330 // Unbind all events bound to window/document by editor instance 6331 if (self.boundEvents) { 6332 var i = self.boundEvents.length; 6333 6334 while (i--) { 6335 var item = self.boundEvents[i]; 6336 this.events.unbind(item[0], item[1], item[2]); 6337 } 6338 6339 self.boundEvents = null; 6340 } 6341 6342 // Restore sizzle document to window.document 6343 // Since the current document might be removed producing "Permission denied" on IE see #6325 6344 if (Sizzle.setDocument) { 6345 Sizzle.setDocument(); 6346 } 6347 6348 self.win = self.doc = self.root = self.events = self.frag = null; 6349 }, 6350 6351 isChildOf: function(node, parent) { 6352 while (node) { 6353 if (parent === node) { 6354 return true; 6355 } 6356 6357 node = node.parentNode; 6358 } 6359 6360 return false; 6361 }, 6362 6363 // #ifdef debug 6364 6365 dumpRng: function(r) { 6366 return ( 6367 'startContainer: ' + r.startContainer.nodeName + 6368 ', startOffset: ' + r.startOffset + 6369 ', endContainer: ' + r.endContainer.nodeName + 6370 ', endOffset: ' + r.endOffset 6371 ); 6372 }, 6373 6374 // #endif 6375 6376 _findSib: function(node, selector, name) { 6377 var self = this, func = selector; 6378 6379 if (node) { 6380 // If expression make a function of it using is 6381 if (typeof(func) == 'string') { 6382 func = function(node) { 6383 return self.is(node, selector); 6384 }; 6385 } 6386 6387 // Loop all siblings 6388 for (node = node[name]; node; node = node[name]) { 6389 if (func(node)) { 6390 return node; 6391 } 6392 } 6393 } 6394 6395 return null; 6396 } 6397 }; 6398 6399 /** 6400 * Instance of DOMUtils for the current document. 6401 * 6402 * @static 6403 * @property DOM 6404 * @type tinymce.dom.DOMUtils 6405 * @example 6406 * // Example of how to add a class to some element by id 6407 * tinymce.DOM.addClass('someid', 'someclass'); 6408 */ 6409 DOMUtils.DOM = new DOMUtils(document); 6410 6411 return DOMUtils; 6412 }); 6413 6414 // Included from: js/tinymce/classes/dom/ScriptLoader.js 6415 6416 /** 6417 * ScriptLoader.js 6418 * 6419 * Copyright, Moxiecode Systems AB 6420 * Released under LGPL License. 6421 * 6422 * License: http://www.tinymce.com/license 6423 * Contributing: http://www.tinymce.com/contributing 6424 */ 6425 6426 /*globals console*/ 6427 6428 /** 6429 * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks 6430 * when various items gets loaded. This class is useful to load external JavaScript files. 6431 * 6432 * @class tinymce.dom.ScriptLoader 6433 * @example 6434 * // Load a script from a specific URL using the global script loader 6435 * tinymce.ScriptLoader.load('somescript.js'); 6436 * 6437 * // Load a script using a unique instance of the script loader 6438 * var scriptLoader = new tinymce.dom.ScriptLoader(); 6439 * 6440 * scriptLoader.load('somescript.js'); 6441 * 6442 * // Load multiple scripts 6443 * var scriptLoader = new tinymce.dom.ScriptLoader(); 6444 * 6445 * scriptLoader.add('somescript1.js'); 6446 * scriptLoader.add('somescript2.js'); 6447 * scriptLoader.add('somescript3.js'); 6448 * 6449 * scriptLoader.loadQueue(function() { 6450 * alert('All scripts are now loaded.'); 6451 * }); 6452 */ 6453 define("tinymce/dom/ScriptLoader", [ 6454 "tinymce/dom/DOMUtils", 6455 "tinymce/util/Tools" 6456 ], function(DOMUtils, Tools) { 6457 var DOM = DOMUtils.DOM; 6458 var each = Tools.each, grep = Tools.grep; 6459 6460 function ScriptLoader() { 6461 var QUEUED = 0, 6462 LOADING = 1, 6463 LOADED = 2, 6464 states = {}, 6465 queue = [], 6466 scriptLoadedCallbacks = {}, 6467 queueLoadedCallbacks = [], 6468 loading = 0, 6469 undef; 6470 6471 /** 6472 * Loads a specific script directly without adding it to the load queue. 6473 * 6474 * @method load 6475 * @param {String} url Absolute URL to script to add. 6476 * @param {function} callback Optional callback function to execute ones this script gets loaded. 6477 * @param {Object} scope Optional scope to execute callback in. 6478 */ 6479 function loadScript(url, callback) { 6480 var dom = DOM, elm, id; 6481 6482 // Execute callback when script is loaded 6483 function done() { 6484 dom.remove(id); 6485 6486 if (elm) { 6487 elm.onreadystatechange = elm.onload = elm = null; 6488 } 6489 6490 callback(); 6491 } 6492 6493 function error() { 6494 /*eslint no-console:0 */ 6495 6496 // Report the error so it's easier for people to spot loading errors 6497 if (typeof(console) !== "undefined" && console.log) { 6498 console.log("Failed to load: " + url); 6499 } 6500 6501 // We can't mark it as done if there is a load error since 6502 // A) We don't want to produce 404 errors on the server and 6503 // B) the onerror event won't fire on all browsers. 6504 // done(); 6505 } 6506 6507 id = dom.uniqueId(); 6508 6509 // Create new script element 6510 elm = document.createElement('script'); 6511 elm.id = id; 6512 elm.type = 'text/javascript'; 6513 elm.src = url; 6514 6515 // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly 6516 if ("onreadystatechange" in elm) { 6517 elm.onreadystatechange = function() { 6518 if (/loaded|complete/.test(elm.readyState)) { 6519 done(); 6520 } 6521 }; 6522 } else { 6523 elm.onload = done; 6524 } 6525 6526 // Add onerror event will get fired on some browsers but not all of them 6527 elm.onerror = error; 6528 6529 // Add script to document 6530 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); 6531 } 6532 6533 /** 6534 * Returns true/false if a script has been loaded or not. 6535 * 6536 * @method isDone 6537 * @param {String} url URL to check for. 6538 * @return {Boolean} true/false if the URL is loaded. 6539 */ 6540 this.isDone = function(url) { 6541 return states[url] == LOADED; 6542 }; 6543 6544 /** 6545 * Marks a specific script to be loaded. This can be useful if a script got loaded outside 6546 * the script loader or to skip it from loading some script. 6547 * 6548 * @method markDone 6549 * @param {string} u Absolute URL to the script to mark as loaded. 6550 */ 6551 this.markDone = function(url) { 6552 states[url] = LOADED; 6553 }; 6554 6555 /** 6556 * Adds a specific script to the load queue of the script loader. 6557 * 6558 * @method add 6559 * @param {String} url Absolute URL to script to add. 6560 * @param {function} callback Optional callback function to execute ones this script gets loaded. 6561 * @param {Object} scope Optional scope to execute callback in. 6562 */ 6563 this.add = this.load = function(url, callback, scope) { 6564 var state = states[url]; 6565 6566 // Add url to load queue 6567 if (state == undef) { 6568 queue.push(url); 6569 states[url] = QUEUED; 6570 } 6571 6572 if (callback) { 6573 // Store away callback for later execution 6574 if (!scriptLoadedCallbacks[url]) { 6575 scriptLoadedCallbacks[url] = []; 6576 } 6577 6578 scriptLoadedCallbacks[url].push({ 6579 func: callback, 6580 scope: scope || this 6581 }); 6582 } 6583 }; 6584 6585 /** 6586 * Starts the loading of the queue. 6587 * 6588 * @method loadQueue 6589 * @param {function} callback Optional callback to execute when all queued items are loaded. 6590 * @param {Object} scope Optional scope to execute the callback in. 6591 */ 6592 this.loadQueue = function(callback, scope) { 6593 this.loadScripts(queue, callback, scope); 6594 }; 6595 6596 /** 6597 * Loads the specified queue of files and executes the callback ones they are loaded. 6598 * This method is generally not used outside this class but it might be useful in some scenarios. 6599 * 6600 * @method loadScripts 6601 * @param {Array} scripts Array of queue items to load. 6602 * @param {function} callback Optional callback to execute ones all items are loaded. 6603 * @param {Object} scope Optional scope to execute callback in. 6604 */ 6605 this.loadScripts = function(scripts, callback, scope) { 6606 var loadScripts; 6607 6608 function execScriptLoadedCallbacks(url) { 6609 // Execute URL callback functions 6610 each(scriptLoadedCallbacks[url], function(callback) { 6611 callback.func.call(callback.scope); 6612 }); 6613 6614 scriptLoadedCallbacks[url] = undef; 6615 } 6616 6617 queueLoadedCallbacks.push({ 6618 func: callback, 6619 scope: scope || this 6620 }); 6621 6622 loadScripts = function() { 6623 var loadingScripts = grep(scripts); 6624 6625 // Current scripts has been handled 6626 scripts.length = 0; 6627 6628 // Load scripts that needs to be loaded 6629 each(loadingScripts, function(url) { 6630 // Script is already loaded then execute script callbacks directly 6631 if (states[url] == LOADED) { 6632 execScriptLoadedCallbacks(url); 6633 return; 6634 } 6635 6636 // Is script not loading then start loading it 6637 if (states[url] != LOADING) { 6638 states[url] = LOADING; 6639 loading++; 6640 6641 loadScript(url, function() { 6642 states[url] = LOADED; 6643 loading--; 6644 6645 execScriptLoadedCallbacks(url); 6646 6647 // Load more scripts if they where added by the recently loaded script 6648 loadScripts(); 6649 }); 6650 } 6651 }); 6652 6653 // No scripts are currently loading then execute all pending queue loaded callbacks 6654 if (!loading) { 6655 each(queueLoadedCallbacks, function(callback) { 6656 callback.func.call(callback.scope); 6657 }); 6658 6659 queueLoadedCallbacks.length = 0; 6660 } 6661 }; 6662 6663 loadScripts(); 6664 }; 6665 } 6666 6667 ScriptLoader.ScriptLoader = new ScriptLoader(); 6668 6669 return ScriptLoader; 6670 }); 6671 6672 // Included from: js/tinymce/classes/AddOnManager.js 6673 6674 /** 6675 * AddOnManager.js 6676 * 6677 * Copyright, Moxiecode Systems AB 6678 * Released under LGPL License. 6679 * 6680 * License: http://www.tinymce.com/license 6681 * Contributing: http://www.tinymce.com/contributing 6682 */ 6683 6684 /** 6685 * This class handles the loading of themes/plugins or other add-ons and their language packs. 6686 * 6687 * @class tinymce.AddOnManager 6688 */ 6689 define("tinymce/AddOnManager", [ 6690 "tinymce/dom/ScriptLoader", 6691 "tinymce/util/Tools" 6692 ], function(ScriptLoader, Tools) { 6693 var each = Tools.each; 6694 6695 function AddOnManager() { 6696 var self = this; 6697 6698 self.items = []; 6699 self.urls = {}; 6700 self.lookup = {}; 6701 } 6702 6703 AddOnManager.prototype = { 6704 /** 6705 * Returns the specified add on by the short name. 6706 * 6707 * @method get 6708 * @param {String} name Add-on to look for. 6709 * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. 6710 */ 6711 get: function(name) { 6712 if (this.lookup[name]) { 6713 return this.lookup[name].instance; 6714 } else { 6715 return undefined; 6716 } 6717 }, 6718 6719 dependencies: function(name) { 6720 var result; 6721 6722 if (this.lookup[name]) { 6723 result = this.lookup[name].dependencies; 6724 } 6725 6726 return result || []; 6727 }, 6728 6729 /** 6730 * Loads a language pack for the specified add-on. 6731 * 6732 * @method requireLangPack 6733 * @param {String} name Short name of the add-on. 6734 * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. 6735 */ 6736 requireLangPack: function(name, languages) { 6737 var language = AddOnManager.language; 6738 6739 if (language && AddOnManager.languageLoad !== false) { 6740 if (languages) { 6741 languages = ',' + languages + ','; 6742 6743 // Load short form sv.js or long form sv_SE.js 6744 if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { 6745 language = language.substr(0, 2); 6746 } else if (languages.indexOf(',' + language + ',') == -1) { 6747 return; 6748 } 6749 } 6750 6751 ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); 6752 } 6753 }, 6754 6755 /** 6756 * Adds a instance of the add-on by it's short name. 6757 * 6758 * @method add 6759 * @param {String} id Short name/id for the add-on. 6760 * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. 6761 * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. 6762 * @example 6763 * // Create a simple plugin 6764 * tinymce.create('tinymce.plugins.TestPlugin', { 6765 * TestPlugin: function(ed, url) { 6766 * ed.on('click', function(e) { 6767 * ed.windowManager.alert('Hello World!'); 6768 * }); 6769 * } 6770 * }); 6771 * 6772 * // Register plugin using the add method 6773 * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); 6774 * 6775 * // Initialize TinyMCE 6776 * tinymce.init({ 6777 * ... 6778 * plugins: '-test' // Init the plugin but don't try to load it 6779 * }); 6780 */ 6781 add: function(id, addOn, dependencies) { 6782 this.items.push(addOn); 6783 this.lookup[id] = {instance: addOn, dependencies: dependencies}; 6784 6785 return addOn; 6786 }, 6787 6788 createUrl: function(baseUrl, dep) { 6789 if (typeof dep === "object") { 6790 return dep; 6791 } else { 6792 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; 6793 } 6794 }, 6795 6796 /** 6797 * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. 6798 * This should be used in development mode. A new compressor/javascript munger process will ensure that the 6799 * components are put together into the plugin.js file and compressed correctly. 6800 * 6801 * @method addComponents 6802 * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). 6803 * @param {Array} scripts Array containing the names of the scripts to load. 6804 */ 6805 addComponents: function(pluginName, scripts) { 6806 var pluginUrl = this.urls[pluginName]; 6807 6808 each(scripts, function(script) { 6809 ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); 6810 }); 6811 }, 6812 6813 /** 6814 * Loads an add-on from a specific url. 6815 * 6816 * @method load 6817 * @param {String} name Short name of the add-on that gets loaded. 6818 * @param {String} addOnUrl URL to the add-on that will get loaded. 6819 * @param {function} callback Optional callback to execute ones the add-on is loaded. 6820 * @param {Object} scope Optional scope to execute the callback in. 6821 * @example 6822 * // Loads a plugin from an external URL 6823 * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); 6824 * 6825 * // Initialize TinyMCE 6826 * tinymce.init({ 6827 * ... 6828 * plugins: '-myplugin' // Don't try to load it again 6829 * }); 6830 */ 6831 load: function(name, addOnUrl, callback, scope) { 6832 var self = this, url = addOnUrl; 6833 6834 function loadDependencies() { 6835 var dependencies = self.dependencies(name); 6836 6837 each(dependencies, function(dep) { 6838 var newUrl = self.createUrl(addOnUrl, dep); 6839 6840 self.load(newUrl.resource, newUrl, undefined, undefined); 6841 }); 6842 6843 if (callback) { 6844 if (scope) { 6845 callback.call(scope); 6846 } else { 6847 callback.call(ScriptLoader); 6848 } 6849 } 6850 } 6851 6852 if (self.urls[name]) { 6853 return; 6854 } 6855 6856 if (typeof addOnUrl === "object") { 6857 url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; 6858 } 6859 6860 if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { 6861 url = AddOnManager.baseURL + '/' + url; 6862 } 6863 6864 self.urls[name] = url.substring(0, url.lastIndexOf('/')); 6865 6866 if (self.lookup[name]) { 6867 loadDependencies(); 6868 } else { 6869 ScriptLoader.ScriptLoader.add(url, loadDependencies, scope); 6870 } 6871 } 6872 }; 6873 6874 AddOnManager.PluginManager = new AddOnManager(); 6875 AddOnManager.ThemeManager = new AddOnManager(); 6876 6877 return AddOnManager; 6878 }); 6879 6880 /** 6881 * TinyMCE theme class. 6882 * 6883 * @class tinymce.Theme 6884 */ 6885 6886 /** 6887 * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. 6888 * 6889 * @method renderUI 6890 * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. 6891 * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. 6892 */ 6893 6894 /** 6895 * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. 6896 * 6897 * @class tinymce.Plugin 6898 * @example 6899 * tinymce.PluginManager.add('example', function(editor, url) { 6900 * // Add a button that opens a window 6901 * editor.addButton('example', { 6902 * text: 'My button', 6903 * icon: false, 6904 * onclick: function() { 6905 * // Open window 6906 * editor.windowManager.open({ 6907 * title: 'Example plugin', 6908 * body: [ 6909 * {type: 'textbox', name: 'title', label: 'Title'} 6910 * ], 6911 * onsubmit: function(e) { 6912 * // Insert content when the window form is submitted 6913 * editor.insertContent('Title: ' + e.data.title); 6914 * } 6915 * }); 6916 * } 6917 * }); 6918 * 6919 * // Adds a menu item to the tools menu 6920 * editor.addMenuItem('example', { 6921 * text: 'Example plugin', 6922 * context: 'tools', 6923 * onclick: function() { 6924 * // Open window with a specific url 6925 * editor.windowManager.open({ 6926 * title: 'TinyMCE site', 6927 * url: 'http://www.tinymce.com', 6928 * width: 800, 6929 * height: 600, 6930 * buttons: [{ 6931 * text: 'Close', 6932 * onclick: 'close' 6933 * }] 6934 * }); 6935 * } 6936 * }); 6937 * }); 6938 */ 6939 6940 // Included from: js/tinymce/classes/dom/RangeUtils.js 6941 6942 /** 6943 * RangeUtils.js 6944 * 6945 * Copyright, Moxiecode Systems AB 6946 * Released under LGPL License. 6947 * 6948 * License: http://www.tinymce.com/license 6949 * Contributing: http://www.tinymce.com/contributing 6950 */ 6951 6952 /** 6953 * This class contains a few utility methods for ranges. 6954 * 6955 * @class tinymce.dom.RangeUtils 6956 * @private 6957 */ 6958 define("tinymce/dom/RangeUtils", [ 6959 "tinymce/util/Tools", 6960 "tinymce/dom/TreeWalker" 6961 ], function(Tools, TreeWalker) { 6962 var each = Tools.each; 6963 6964 function getEndChild(container, index) { 6965 var childNodes = container.childNodes; 6966 6967 index--; 6968 6969 if (index > childNodes.length - 1) { 6970 index = childNodes.length - 1; 6971 } else if (index < 0) { 6972 index = 0; 6973 } 6974 6975 return childNodes[index] || container; 6976 } 6977 6978 function RangeUtils(dom) { 6979 /** 6980 * Walks the specified range like object and executes the callback for each sibling collection it finds. 6981 * 6982 * @method walk 6983 * @param {Object} rng Range like object. 6984 * @param {function} callback Callback function to execute for each sibling collection. 6985 */ 6986 this.walk = function(rng, callback) { 6987 var startContainer = rng.startContainer, 6988 startOffset = rng.startOffset, 6989 endContainer = rng.endContainer, 6990 endOffset = rng.endOffset, 6991 ancestor, startPoint, 6992 endPoint, node, parent, siblings, nodes; 6993 6994 // Handle table cell selection the table plugin enables 6995 // you to fake select table cells and perform formatting actions on them 6996 nodes = dom.select('td.mce-item-selected,th.mce-item-selected'); 6997 if (nodes.length > 0) { 6998 each(nodes, function(node) { 6999 callback([node]); 7000 }); 7001 7002 return; 7003 } 7004 7005 /** 7006 * Excludes start/end text node if they are out side the range 7007 * 7008 * @private 7009 * @param {Array} nodes Nodes to exclude items from. 7010 * @return {Array} Array with nodes excluding the start/end container if needed. 7011 */ 7012 function exclude(nodes) { 7013 var node; 7014 7015 // First node is excluded 7016 node = nodes[0]; 7017 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { 7018 nodes.splice(0, 1); 7019 } 7020 7021 // Last node is excluded 7022 node = nodes[nodes.length - 1]; 7023 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { 7024 nodes.splice(nodes.length - 1, 1); 7025 } 7026 7027 return nodes; 7028 } 7029 7030 /** 7031 * Collects siblings 7032 * 7033 * @private 7034 * @param {Node} node Node to collect siblings from. 7035 * @param {String} name Name of the sibling to check for. 7036 * @return {Array} Array of collected siblings. 7037 */ 7038 function collectSiblings(node, name, end_node) { 7039 var siblings = []; 7040 7041 for (; node && node != end_node; node = node[name]) { 7042 siblings.push(node); 7043 } 7044 7045 return siblings; 7046 } 7047 7048 /** 7049 * Find an end point this is the node just before the common ancestor root. 7050 * 7051 * @private 7052 * @param {Node} node Node to start at. 7053 * @param {Node} root Root/ancestor element to stop just before. 7054 * @return {Node} Node just before the root element. 7055 */ 7056 function findEndPoint(node, root) { 7057 do { 7058 if (node.parentNode == root) { 7059 return node; 7060 } 7061 7062 node = node.parentNode; 7063 } while (node); 7064 } 7065 7066 function walkBoundary(start_node, end_node, next) { 7067 var siblingName = next ? 'nextSibling' : 'previousSibling'; 7068 7069 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { 7070 parent = node.parentNode; 7071 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); 7072 7073 if (siblings.length) { 7074 if (!next) { 7075 siblings.reverse(); 7076 } 7077 7078 callback(exclude(siblings)); 7079 } 7080 } 7081 } 7082 7083 // If index based start position then resolve it 7084 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 7085 startContainer = startContainer.childNodes[startOffset]; 7086 } 7087 7088 // If index based end position then resolve it 7089 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 7090 endContainer = getEndChild(endContainer, endOffset); 7091 } 7092 7093 // Same container 7094 if (startContainer == endContainer) { 7095 return callback(exclude([startContainer])); 7096 } 7097 7098 // Find common ancestor and end points 7099 ancestor = dom.findCommonAncestor(startContainer, endContainer); 7100 7101 // Process left side 7102 for (node = startContainer; node; node = node.parentNode) { 7103 if (node === endContainer) { 7104 return walkBoundary(startContainer, ancestor, true); 7105 } 7106 7107 if (node === ancestor) { 7108 break; 7109 } 7110 } 7111 7112 // Process right side 7113 for (node = endContainer; node; node = node.parentNode) { 7114 if (node === startContainer) { 7115 return walkBoundary(endContainer, ancestor); 7116 } 7117 7118 if (node === ancestor) { 7119 break; 7120 } 7121 } 7122 7123 // Find start/end point 7124 startPoint = findEndPoint(startContainer, ancestor) || startContainer; 7125 endPoint = findEndPoint(endContainer, ancestor) || endContainer; 7126 7127 // Walk left leaf 7128 walkBoundary(startContainer, startPoint, true); 7129 7130 // Walk the middle from start to end point 7131 siblings = collectSiblings( 7132 startPoint == startContainer ? startPoint : startPoint.nextSibling, 7133 'nextSibling', 7134 endPoint == endContainer ? endPoint.nextSibling : endPoint 7135 ); 7136 7137 if (siblings.length) { 7138 callback(exclude(siblings)); 7139 } 7140 7141 // Walk right leaf 7142 walkBoundary(endContainer, endPoint); 7143 }; 7144 7145 /** 7146 * Splits the specified range at it's start/end points. 7147 * 7148 * @private 7149 * @param {Range/RangeObject} rng Range to split. 7150 * @return {Object} Range position object. 7151 */ 7152 this.split = function(rng) { 7153 var startContainer = rng.startContainer, 7154 startOffset = rng.startOffset, 7155 endContainer = rng.endContainer, 7156 endOffset = rng.endOffset; 7157 7158 function splitText(node, offset) { 7159 return node.splitText(offset); 7160 } 7161 7162 // Handle single text node 7163 if (startContainer == endContainer && startContainer.nodeType == 3) { 7164 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { 7165 endContainer = splitText(startContainer, startOffset); 7166 startContainer = endContainer.previousSibling; 7167 7168 if (endOffset > startOffset) { 7169 endOffset = endOffset - startOffset; 7170 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; 7171 endOffset = endContainer.nodeValue.length; 7172 startOffset = 0; 7173 } else { 7174 endOffset = 0; 7175 } 7176 } 7177 } else { 7178 // Split startContainer text node if needed 7179 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { 7180 startContainer = splitText(startContainer, startOffset); 7181 startOffset = 0; 7182 } 7183 7184 // Split endContainer text node if needed 7185 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { 7186 endContainer = splitText(endContainer, endOffset).previousSibling; 7187 endOffset = endContainer.nodeValue.length; 7188 } 7189 } 7190 7191 return { 7192 startContainer: startContainer, 7193 startOffset: startOffset, 7194 endContainer: endContainer, 7195 endOffset: endOffset 7196 }; 7197 }; 7198 7199 /** 7200 * Normalizes the specified range by finding the closest best suitable caret location. 7201 * 7202 * @private 7203 * @param {Range} rng Range to normalize. 7204 * @return {Boolean} True/false if the specified range was normalized or not. 7205 */ 7206 this.normalize = function(rng) { 7207 var normalized, collapsed; 7208 7209 function normalizeEndPoint(start) { 7210 var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; 7211 var directionLeft, isAfterNode; 7212 7213 function hasBrBeforeAfter(node, left) { 7214 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); 7215 7216 while ((node = walker[left ? 'prev' : 'next']())) { 7217 if (node.nodeName === "BR") { 7218 return true; 7219 } 7220 } 7221 } 7222 7223 function isPrevNode(node, name) { 7224 return node.previousSibling && node.previousSibling.nodeName == name; 7225 } 7226 7227 // Walks the dom left/right to find a suitable text node to move the endpoint into 7228 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 7229 function findTextNodeRelative(left, startNode) { 7230 var walker, lastInlineElement, parentBlockContainer; 7231 7232 startNode = startNode || container; 7233 parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; 7234 7235 // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 7236 // This: <p><br>|</p> becomes <p>|<br></p> 7237 if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) { 7238 container = startNode.parentNode; 7239 offset = dom.nodeIndex(startNode); 7240 normalized = true; 7241 return; 7242 } 7243 7244 // Walk left until we hit a text node we can move to or a block/br/img 7245 walker = new TreeWalker(startNode, parentBlockContainer); 7246 while ((node = walker[left ? 'prev' : 'next']())) { 7247 // Break if we hit a non content editable node 7248 if (dom.getContentEditableParent(node) === "false") { 7249 return; 7250 } 7251 7252 // Found text node that has a length 7253 if (node.nodeType === 3 && node.nodeValue.length > 0) { 7254 container = node; 7255 offset = left ? node.nodeValue.length : 0; 7256 normalized = true; 7257 return; 7258 } 7259 7260 // Break if we find a block or a BR/IMG/INPUT etc 7261 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 7262 return; 7263 } 7264 7265 lastInlineElement = node; 7266 } 7267 7268 // Only fetch the last inline element when in caret mode for now 7269 if (collapsed && lastInlineElement) { 7270 container = lastInlineElement; 7271 normalized = true; 7272 offset = 0; 7273 } 7274 } 7275 7276 container = rng[(start ? 'start' : 'end') + 'Container']; 7277 offset = rng[(start ? 'start' : 'end') + 'Offset']; 7278 isAfterNode = container.nodeType == 1 && offset === container.childNodes.length; 7279 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 7280 directionLeft = start; 7281 7282 if (container.nodeType == 1 && offset > container.childNodes.length - 1) { 7283 directionLeft = false; 7284 } 7285 7286 // If the container is a document move it to the body element 7287 if (container.nodeType === 9) { 7288 container = dom.getRoot(); 7289 offset = 0; 7290 } 7291 7292 // If the container is body try move it into the closest text node or position 7293 if (container === body) { 7294 // If start is before/after a image, table etc 7295 if (directionLeft) { 7296 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 7297 if (node) { 7298 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { 7299 return; 7300 } 7301 } 7302 } 7303 7304 // Resolve the index 7305 if (container.hasChildNodes()) { 7306 offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); 7307 container = container.childNodes[offset]; 7308 offset = 0; 7309 7310 // Don't walk into elements that doesn't have any child nodes like a IMG 7311 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { 7312 // Walk the DOM to find a text node to place the caret at or a BR 7313 node = container; 7314 walker = new TreeWalker(container, body); 7315 7316 do { 7317 // Found a text node use that position 7318 if (node.nodeType === 3 && node.nodeValue.length > 0) { 7319 offset = directionLeft ? 0 : node.nodeValue.length; 7320 container = node; 7321 normalized = true; 7322 break; 7323 } 7324 7325 // Found a BR/IMG element that we can place the caret before 7326 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 7327 offset = dom.nodeIndex(node); 7328 container = node.parentNode; 7329 7330 // Put caret after image when moving the end point 7331 if (node.nodeName == "IMG" && !directionLeft) { 7332 offset++; 7333 } 7334 7335 normalized = true; 7336 break; 7337 } 7338 } while ((node = (directionLeft ? walker.next() : walker.prev()))); 7339 } 7340 } 7341 } 7342 7343 // Lean the caret to the left if possible 7344 if (collapsed) { 7345 // So this: <b>x</b><i>|x</i> 7346 // Becomes: <b>x|</b><i>x</i> 7347 // Seems that only gecko has issues with this 7348 if (container.nodeType === 3 && offset === 0) { 7349 findTextNodeRelative(true); 7350 } 7351 7352 // Lean left into empty inline elements when the caret is before a BR 7353 // So this: <i><b></b><i>|<br></i> 7354 // Becomes: <i><b>|</b><i><br></i> 7355 // Seems that only gecko has issues with this. 7356 // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p> 7357 if (container.nodeType === 1) { 7358 node = container.childNodes[offset]; 7359 7360 // Offset is after the containers last child 7361 // then use the previous child for normalization 7362 if (!node) { 7363 node = container.childNodes[offset - 1]; 7364 } 7365 7366 if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') && 7367 !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { 7368 findTextNodeRelative(true, node); 7369 } 7370 } 7371 } 7372 7373 // Lean the start of the selection right if possible 7374 // So this: x[<b>x]</b> 7375 // Becomes: x<b>[x]</b> 7376 if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { 7377 findTextNodeRelative(false); 7378 } 7379 7380 // Set endpoint if it was normalized 7381 if (normalized) { 7382 rng['set' + (start ? 'Start' : 'End')](container, offset); 7383 } 7384 } 7385 7386 collapsed = rng.collapsed; 7387 7388 normalizeEndPoint(true); 7389 7390 if (!collapsed) { 7391 normalizeEndPoint(); 7392 } 7393 7394 // If it was collapsed then make sure it still is 7395 if (normalized && collapsed) { 7396 rng.collapse(true); 7397 } 7398 7399 return normalized; 7400 }; 7401 } 7402 7403 /** 7404 * Compares two ranges and checks if they are equal. 7405 * 7406 * @static 7407 * @method compareRanges 7408 * @param {DOMRange} rng1 First range to compare. 7409 * @param {DOMRange} rng2 First range to compare. 7410 * @return {Boolean} true/false if the ranges are equal. 7411 */ 7412 RangeUtils.compareRanges = function(rng1, rng2) { 7413 if (rng1 && rng2) { 7414 // Compare native IE ranges 7415 if (rng1.item || rng1.duplicate) { 7416 // Both are control ranges and the selected element matches 7417 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) { 7418 return true; 7419 } 7420 7421 // Both are text ranges and the range matches 7422 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) { 7423 return true; 7424 } 7425 } else { 7426 // Compare w3c ranges 7427 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; 7428 } 7429 } 7430 7431 return false; 7432 }; 7433 7434 return RangeUtils; 7435 }); 7436 7437 // Included from: js/tinymce/classes/NodeChange.js 7438 7439 /** 7440 * NodeChange.js 7441 * 7442 * Copyright, Moxiecode Systems AB 7443 * Released under LGPL License. 7444 * 7445 * License: http://www.tinymce.com/license 7446 * Contributing: http://www.tinymce.com/contributing 7447 */ 7448 7449 /** 7450 * This class handles the nodechange event dispatching both manual and though selection change events. 7451 * 7452 * @class tinymce.NodeChange 7453 * @private 7454 */ 7455 define("tinymce/NodeChange", [ 7456 "tinymce/dom/RangeUtils" 7457 ], function(RangeUtils) { 7458 return function(editor) { 7459 var lastRng, lastPath = []; 7460 7461 /** 7462 * Returns true/false if the current element path has been changed or not. 7463 * 7464 * @private 7465 * @return {Boolean} True if the element path is the same false if it's not. 7466 */ 7467 function isSameElementPath(startElm) { 7468 var i, currentPath; 7469 7470 currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm); 7471 if (currentPath.length === lastPath.length) { 7472 for (i = currentPath.length; i >= 0; i--) { 7473 if (currentPath[i] !== lastPath[i]) { 7474 break; 7475 } 7476 } 7477 7478 if (i === -1) { 7479 lastPath = currentPath; 7480 return true; 7481 } 7482 } 7483 7484 lastPath = currentPath; 7485 7486 return false; 7487 } 7488 7489 // Gecko doesn't support the "selectionchange" event 7490 if (!('onselectionchange' in editor.getDoc())) { 7491 editor.on('NodeChange Click MouseUp KeyUp Focus', function(e) { 7492 var nativeRng, fakeRng; 7493 7494 // Since DOM Ranges mutate on modification 7495 // of the DOM we need to clone it's contents 7496 nativeRng = editor.selection.getRng(); 7497 fakeRng = { 7498 startContainer: nativeRng.startContainer, 7499 startOffset: nativeRng.startOffset, 7500 endContainer: nativeRng.endContainer, 7501 endOffset: nativeRng.endOffset 7502 }; 7503 7504 // Always treat nodechange as a selectionchange since applying 7505 // formatting to the current range wouldn't update the range but it's parent 7506 if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) { 7507 editor.fire('SelectionChange'); 7508 } 7509 7510 lastRng = fakeRng; 7511 }); 7512 } 7513 7514 // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body 7515 // When the contextmenu event fires the selection is located at the right location 7516 editor.on('contextmenu', function() { 7517 editor.fire('SelectionChange'); 7518 }); 7519 7520 editor.on('SelectionChange', function() { 7521 var startElm = editor.selection.getStart(true); 7522 7523 // Fire a nodechange only when the selection isn't collapsed since focusout will collapse and remove the selection 7524 if (!editor.selection.isCollapsed() && !isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) { 7525 editor.nodeChanged({selectionChange: true}); 7526 } 7527 }); 7528 7529 // Fire an extra nodeChange on mouseup for compatibility reasons 7530 editor.on('MouseUp', function(e) { 7531 if (!e.isDefaultPrevented()) { 7532 // Delay nodeChanged call for WebKit edge case issue where the range 7533 // isn't updated until after you click outside a selected image 7534 setTimeout(function() { 7535 editor.nodeChanged(); 7536 }, 0); 7537 } 7538 }); 7539 7540 /** 7541 * Distpaches out a onNodeChange event to all observers. This method should be called when you 7542 * need to update the UI states or element path etc. 7543 * 7544 * @method nodeChanged 7545 * @param {Object} args Optional args to pass to NodeChange event handlers. 7546 */ 7547 this.nodeChanged = function(args) { 7548 var selection = editor.selection, node, parents, root; 7549 7550 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 7551 if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.settings.readonly) { 7552 // Get start node 7553 root = editor.getBody(); 7554 node = selection.getStart() || root; 7555 node = node.ownerDocument != editor.getDoc() ? editor.getBody() : node; 7556 7557 // Edge case for <p>|<img></p> 7558 if (node.nodeName == 'IMG' && selection.isCollapsed()) { 7559 node = node.parentNode; 7560 } 7561 7562 // Get parents and add them to object 7563 parents = []; 7564 editor.dom.getParent(node, function(node) { 7565 if (node === root) { 7566 return true; 7567 } 7568 7569 parents.push(node); 7570 }); 7571 7572 args = args || {}; 7573 args.element = node; 7574 args.parents = parents; 7575 7576 editor.fire('NodeChange', args); 7577 } 7578 }; 7579 }; 7580 }); 7581 7582 // Included from: js/tinymce/classes/html/Node.js 7583 7584 /** 7585 * Node.js 7586 * 7587 * Copyright, Moxiecode Systems AB 7588 * Released under LGPL License. 7589 * 7590 * License: http://www.tinymce.com/license 7591 * Contributing: http://www.tinymce.com/contributing 7592 */ 7593 7594 /** 7595 * This class is a minimalistic implementation of a DOM like node used by the DomParser class. 7596 * 7597 * @example 7598 * var node = new tinymce.html.Node('strong', 1); 7599 * someRoot.append(node); 7600 * 7601 * @class tinymce.html.Node 7602 * @version 3.4 7603 */ 7604 define("tinymce/html/Node", [], function() { 7605 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { 7606 '#text': 3, 7607 '#comment': 8, 7608 '#cdata': 4, 7609 '#pi': 7, 7610 '#doctype': 10, 7611 '#document-fragment': 11 7612 }; 7613 7614 // Walks the tree left/right 7615 function walk(node, root_node, prev) { 7616 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; 7617 7618 // Walk into nodes if it has a start 7619 if (node[startName]) { 7620 return node[startName]; 7621 } 7622 7623 // Return the sibling if it has one 7624 if (node !== root_node) { 7625 sibling = node[siblingName]; 7626 7627 if (sibling) { 7628 return sibling; 7629 } 7630 7631 // Walk up the parents to look for siblings 7632 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { 7633 sibling = parent[siblingName]; 7634 7635 if (sibling) { 7636 return sibling; 7637 } 7638 } 7639 } 7640 } 7641 7642 /** 7643 * Constructs a new Node instance. 7644 * 7645 * @constructor 7646 * @method Node 7647 * @param {String} name Name of the node type. 7648 * @param {Number} type Numeric type representing the node. 7649 */ 7650 function Node(name, type) { 7651 this.name = name; 7652 this.type = type; 7653 7654 if (type === 1) { 7655 this.attributes = []; 7656 this.attributes.map = {}; 7657 } 7658 } 7659 7660 Node.prototype = { 7661 /** 7662 * Replaces the current node with the specified one. 7663 * 7664 * @example 7665 * someNode.replace(someNewNode); 7666 * 7667 * @method replace 7668 * @param {tinymce.html.Node} node Node to replace the current node with. 7669 * @return {tinymce.html.Node} The old node that got replaced. 7670 */ 7671 replace: function(node) { 7672 var self = this; 7673 7674 if (node.parent) { 7675 node.remove(); 7676 } 7677 7678 self.insert(node, self); 7679 self.remove(); 7680 7681 return self; 7682 }, 7683 7684 /** 7685 * Gets/sets or removes an attribute by name. 7686 * 7687 * @example 7688 * someNode.attr("name", "value"); // Sets an attribute 7689 * console.log(someNode.attr("name")); // Gets an attribute 7690 * someNode.attr("name", null); // Removes an attribute 7691 * 7692 * @method attr 7693 * @param {String} name Attribute name to set or get. 7694 * @param {String} value Optional value to set. 7695 * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. 7696 */ 7697 attr: function(name, value) { 7698 var self = this, attrs, i, undef; 7699 7700 if (typeof name !== "string") { 7701 for (i in name) { 7702 self.attr(i, name[i]); 7703 } 7704 7705 return self; 7706 } 7707 7708 if ((attrs = self.attributes)) { 7709 if (value !== undef) { 7710 // Remove attribute 7711 if (value === null) { 7712 if (name in attrs.map) { 7713 delete attrs.map[name]; 7714 7715 i = attrs.length; 7716 while (i--) { 7717 if (attrs[i].name === name) { 7718 attrs = attrs.splice(i, 1); 7719 return self; 7720 } 7721 } 7722 } 7723 7724 return self; 7725 } 7726 7727 // Set attribute 7728 if (name in attrs.map) { 7729 // Set attribute 7730 i = attrs.length; 7731 while (i--) { 7732 if (attrs[i].name === name) { 7733 attrs[i].value = value; 7734 break; 7735 } 7736 } 7737 } else { 7738 attrs.push({name: name, value: value}); 7739 } 7740 7741 attrs.map[name] = value; 7742 7743 return self; 7744 } else { 7745 return attrs.map[name]; 7746 } 7747 } 7748 }, 7749 7750 /** 7751 * Does a shallow clones the node into a new node. It will also exclude id attributes since 7752 * there should only be one id per document. 7753 * 7754 * @example 7755 * var clonedNode = node.clone(); 7756 * 7757 * @method clone 7758 * @return {tinymce.html.Node} New copy of the original node. 7759 */ 7760 clone: function() { 7761 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; 7762 7763 // Clone element attributes 7764 if ((selfAttrs = self.attributes)) { 7765 cloneAttrs = []; 7766 cloneAttrs.map = {}; 7767 7768 for (i = 0, l = selfAttrs.length; i < l; i++) { 7769 selfAttr = selfAttrs[i]; 7770 7771 // Clone everything except id 7772 if (selfAttr.name !== 'id') { 7773 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; 7774 cloneAttrs.map[selfAttr.name] = selfAttr.value; 7775 } 7776 } 7777 7778 clone.attributes = cloneAttrs; 7779 } 7780 7781 clone.value = self.value; 7782 clone.shortEnded = self.shortEnded; 7783 7784 return clone; 7785 }, 7786 7787 /** 7788 * Wraps the node in in another node. 7789 * 7790 * @example 7791 * node.wrap(wrapperNode); 7792 * 7793 * @method wrap 7794 */ 7795 wrap: function(wrapper) { 7796 var self = this; 7797 7798 self.parent.insert(wrapper, self); 7799 wrapper.append(self); 7800 7801 return self; 7802 }, 7803 7804 /** 7805 * Unwraps the node in other words it removes the node but keeps the children. 7806 * 7807 * @example 7808 * node.unwrap(); 7809 * 7810 * @method unwrap 7811 */ 7812 unwrap: function() { 7813 var self = this, node, next; 7814 7815 for (node = self.firstChild; node;) { 7816 next = node.next; 7817 self.insert(node, self, true); 7818 node = next; 7819 } 7820 7821 self.remove(); 7822 }, 7823 7824 /** 7825 * Removes the node from it's parent. 7826 * 7827 * @example 7828 * node.remove(); 7829 * 7830 * @method remove 7831 * @return {tinymce.html.Node} Current node that got removed. 7832 */ 7833 remove: function() { 7834 var self = this, parent = self.parent, next = self.next, prev = self.prev; 7835 7836 if (parent) { 7837 if (parent.firstChild === self) { 7838 parent.firstChild = next; 7839 7840 if (next) { 7841 next.prev = null; 7842 } 7843 } else { 7844 prev.next = next; 7845 } 7846 7847 if (parent.lastChild === self) { 7848 parent.lastChild = prev; 7849 7850 if (prev) { 7851 prev.next = null; 7852 } 7853 } else { 7854 next.prev = prev; 7855 } 7856 7857 self.parent = self.next = self.prev = null; 7858 } 7859 7860 return self; 7861 }, 7862 7863 /** 7864 * Appends a new node as a child of the current node. 7865 * 7866 * @example 7867 * node.append(someNode); 7868 * 7869 * @method append 7870 * @param {tinymce.html.Node} node Node to append as a child of the current one. 7871 * @return {tinymce.html.Node} The node that got appended. 7872 */ 7873 append: function(node) { 7874 var self = this, last; 7875 7876 if (node.parent) { 7877 node.remove(); 7878 } 7879 7880 last = self.lastChild; 7881 if (last) { 7882 last.next = node; 7883 node.prev = last; 7884 self.lastChild = node; 7885 } else { 7886 self.lastChild = self.firstChild = node; 7887 } 7888 7889 node.parent = self; 7890 7891 return node; 7892 }, 7893 7894 /** 7895 * Inserts a node at a specific position as a child of the current node. 7896 * 7897 * @example 7898 * parentNode.insert(newChildNode, oldChildNode); 7899 * 7900 * @method insert 7901 * @param {tinymce.html.Node} node Node to insert as a child of the current node. 7902 * @param {tinymce.html.Node} ref_node Reference node to set node before/after. 7903 * @param {Boolean} before Optional state to insert the node before the reference node. 7904 * @return {tinymce.html.Node} The node that got inserted. 7905 */ 7906 insert: function(node, ref_node, before) { 7907 var parent; 7908 7909 if (node.parent) { 7910 node.remove(); 7911 } 7912 7913 parent = ref_node.parent || this; 7914 7915 if (before) { 7916 if (ref_node === parent.firstChild) { 7917 parent.firstChild = node; 7918 } else { 7919 ref_node.prev.next = node; 7920 } 7921 7922 node.prev = ref_node.prev; 7923 node.next = ref_node; 7924 ref_node.prev = node; 7925 } else { 7926 if (ref_node === parent.lastChild) { 7927 parent.lastChild = node; 7928 } else { 7929 ref_node.next.prev = node; 7930 } 7931 7932 node.next = ref_node.next; 7933 node.prev = ref_node; 7934 ref_node.next = node; 7935 } 7936 7937 node.parent = parent; 7938 7939 return node; 7940 }, 7941 7942 /** 7943 * Get all children by name. 7944 * 7945 * @method getAll 7946 * @param {String} name Name of the child nodes to collect. 7947 * @return {Array} Array with child nodes matchin the specified name. 7948 */ 7949 getAll: function(name) { 7950 var self = this, node, collection = []; 7951 7952 for (node = self.firstChild; node; node = walk(node, self)) { 7953 if (node.name === name) { 7954 collection.push(node); 7955 } 7956 } 7957 7958 return collection; 7959 }, 7960 7961 /** 7962 * Removes all children of the current node. 7963 * 7964 * @method empty 7965 * @return {tinymce.html.Node} The current node that got cleared. 7966 */ 7967 empty: function() { 7968 var self = this, nodes, i, node; 7969 7970 // Remove all children 7971 if (self.firstChild) { 7972 nodes = []; 7973 7974 // Collect the children 7975 for (node = self.firstChild; node; node = walk(node, self)) { 7976 nodes.push(node); 7977 } 7978 7979 // Remove the children 7980 i = nodes.length; 7981 while (i--) { 7982 node = nodes[i]; 7983 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 7984 } 7985 } 7986 7987 self.firstChild = self.lastChild = null; 7988 7989 return self; 7990 }, 7991 7992 /** 7993 * Returns true/false if the node is to be considered empty or not. 7994 * 7995 * @example 7996 * node.isEmpty({img: true}); 7997 * @method isEmpty 7998 * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. 7999 * @return {Boolean} true/false if the node is empty or not. 8000 */ 8001 isEmpty: function(elements) { 8002 var self = this, node = self.firstChild, i, name; 8003 8004 if (node) { 8005 do { 8006 if (node.type === 1) { 8007 // Ignore bogus elements 8008 if (node.attributes.map['data-mce-bogus']) { 8009 continue; 8010 } 8011 8012 // Keep empty elements like <img /> 8013 if (elements[node.name]) { 8014 return false; 8015 } 8016 8017 // Keep bookmark nodes and name attribute like <a name="1"></a> 8018 i = node.attributes.length; 8019 while (i--) { 8020 name = node.attributes[i].name; 8021 if (name === "name" || name.indexOf('data-mce-bookmark') === 0) { 8022 return false; 8023 } 8024 } 8025 } 8026 8027 // Keep comments 8028 if (node.type === 8) { 8029 return false; 8030 } 8031 8032 // Keep non whitespace text nodes 8033 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) { 8034 return false; 8035 } 8036 } while ((node = walk(node, self))); 8037 } 8038 8039 return true; 8040 }, 8041 8042 /** 8043 * Walks to the next or previous node and returns that node or null if it wasn't found. 8044 * 8045 * @method walk 8046 * @param {Boolean} prev Optional previous node state defaults to false. 8047 * @return {tinymce.html.Node} Node that is next to or previous of the current node. 8048 */ 8049 walk: function(prev) { 8050 return walk(this, null, prev); 8051 } 8052 }; 8053 8054 /** 8055 * Creates a node of a specific type. 8056 * 8057 * @static 8058 * @method create 8059 * @param {String} name Name of the node type to create for example "b" or "#text". 8060 * @param {Object} attrs Name/value collection of attributes that will be applied to elements. 8061 */ 8062 Node.create = function(name, attrs) { 8063 var node, attrName; 8064 8065 // Create node 8066 node = new Node(name, typeLookup[name] || 1); 8067 8068 // Add attributes if needed 8069 if (attrs) { 8070 for (attrName in attrs) { 8071 node.attr(attrName, attrs[attrName]); 8072 } 8073 } 8074 8075 return node; 8076 }; 8077 8078 return Node; 8079 }); 8080 8081 // Included from: js/tinymce/classes/html/Schema.js 8082 8083 /** 8084 * Schema.js 8085 * 8086 * Copyright, Moxiecode Systems AB 8087 * Released under LGPL License. 8088 * 8089 * License: http://www.tinymce.com/license 8090 * Contributing: http://www.tinymce.com/contributing 8091 */ 8092 8093 /** 8094 * Schema validator class. 8095 * 8096 * @class tinymce.html.Schema 8097 * @example 8098 * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) 8099 * alert('span is valid child of p.'); 8100 * 8101 * if (tinymce.activeEditor.schema.getElementRule('p')) 8102 * alert('P is a valid element.'); 8103 * 8104 * @class tinymce.html.Schema 8105 * @version 3.4 8106 */ 8107 define("tinymce/html/Schema", [ 8108 "tinymce/util/Tools" 8109 ], function(Tools) { 8110 var mapCache = {}, dummyObj = {}; 8111 var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; 8112 8113 function split(items, delim) { 8114 return items ? items.split(delim || ' ') : []; 8115 } 8116 8117 /** 8118 * Builds a schema lookup table 8119 * 8120 * @private 8121 * @param {String} type html4, html5 or html5-strict schema type. 8122 * @return {Object} Schema lookup table. 8123 */ 8124 function compileSchema(type) { 8125 var schema = {}, globalAttributes, blockContent; 8126 var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; 8127 8128 function add(name, attributes, children) { 8129 var ni, i, attributesOrder, args = arguments; 8130 8131 function arrayToMap(array, obj) { 8132 var map = {}, i, l; 8133 8134 for (i = 0, l = array.length; i < l; i++) { 8135 map[array[i]] = obj || {}; 8136 } 8137 8138 return map; 8139 } 8140 8141 children = children || []; 8142 attributes = attributes || ""; 8143 8144 if (typeof(children) === "string") { 8145 children = split(children); 8146 } 8147 8148 // Split string children 8149 for (i = 3; i < args.length; i++) { 8150 if (typeof(args[i]) === "string") { 8151 args[i] = split(args[i]); 8152 } 8153 8154 children.push.apply(children, args[i]); 8155 } 8156 8157 name = split(name); 8158 ni = name.length; 8159 while (ni--) { 8160 attributesOrder = [].concat(globalAttributes, split(attributes)); 8161 schema[name[ni]] = { 8162 attributes: arrayToMap(attributesOrder), 8163 attributesOrder: attributesOrder, 8164 children: arrayToMap(children, dummyObj) 8165 }; 8166 } 8167 } 8168 8169 function addAttrs(name, attributes) { 8170 var ni, schemaItem, i, l; 8171 8172 name = split(name); 8173 ni = name.length; 8174 attributes = split(attributes); 8175 while (ni--) { 8176 schemaItem = schema[name[ni]]; 8177 for (i = 0, l = attributes.length; i < l; i++) { 8178 schemaItem.attributes[attributes[i]] = {}; 8179 schemaItem.attributesOrder.push(attributes[i]); 8180 } 8181 } 8182 } 8183 8184 // Use cached schema 8185 if (mapCache[type]) { 8186 return mapCache[type]; 8187 } 8188 8189 // Attributes present on all elements 8190 globalAttributes = split("id accesskey class dir lang style tabindex title"); 8191 8192 // Event attributes can be opt-in/opt-out 8193 /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + 8194 "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + 8195 "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + 8196 "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + 8197 "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + 8198 "onwaiting" 8199 );*/ 8200 8201 // Block content elements 8202 blockContent = split( 8203 "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul" 8204 ); 8205 8206 // Phrasing content elements from the HTML5 spec (inline) 8207 phrasingContent = split( 8208 "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + 8209 "label map noscript object q s samp script select small span strong sub sup " + 8210 "textarea u var #text #comment" 8211 ); 8212 8213 // Add HTML5 items to globalAttributes, blockContent, phrasingContent 8214 if (type != "html4") { 8215 globalAttributes.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " + 8216 "hidden spellcheck translate")); 8217 blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav")); 8218 phrasingContent.push.apply(phrasingContent, split("audio canvas command datalist mark meter output progress time wbr " + 8219 "video ruby bdi keygen")); 8220 } 8221 8222 // Add HTML4 elements unless it's html5-strict 8223 if (type != "html5-strict") { 8224 globalAttributes.push("xml:lang"); 8225 8226 html4PhrasingContent = split("acronym applet basefont big font strike tt"); 8227 phrasingContent.push.apply(phrasingContent, html4PhrasingContent); 8228 8229 each(html4PhrasingContent, function(name) { 8230 add(name, "", phrasingContent); 8231 }); 8232 8233 html4BlockContent = split("center dir isindex noframes"); 8234 blockContent.push.apply(blockContent, html4BlockContent); 8235 8236 // Flow content elements from the HTML5 spec (block+inline) 8237 flowContent = [].concat(blockContent, phrasingContent); 8238 8239 each(html4BlockContent, function(name) { 8240 add(name, "", flowContent); 8241 }); 8242 } 8243 8244 // Flow content elements from the HTML5 spec (block+inline) 8245 flowContent = flowContent || [].concat(blockContent, phrasingContent); 8246 8247 // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement 8248 // Schema items <element name>, <specific attributes>, <children ..> 8249 add("html", "manifest", "head body"); 8250 add("head", "", "base command link meta noscript script style title"); 8251 add("title hr noscript br"); 8252 add("base", "href target"); 8253 add("link", "href rel media hreflang type sizes hreflang"); 8254 add("meta", "name http-equiv content charset"); 8255 add("style", "media type scoped"); 8256 add("script", "src async defer type charset"); 8257 add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " + 8258 "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " + 8259 "onpopstate onresize onscroll onstorage onunload", flowContent); 8260 add("address dt dd div caption", "", flowContent); 8261 add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent); 8262 add("blockquote", "cite", flowContent); 8263 add("ol", "reversed start type", "li"); 8264 add("ul", "", "li"); 8265 add("li", "value", flowContent); 8266 add("dl", "", "dt dd"); 8267 add("a", "href target rel media hreflang type", phrasingContent); 8268 add("q", "cite", phrasingContent); 8269 add("ins del", "cite datetime", flowContent); 8270 add("img", "src alt usemap ismap width height"); 8271 add("iframe", "src name width height", flowContent); 8272 add("embed", "src type width height"); 8273 add("object", "data type typemustmatch name usemap form width height", flowContent, "param"); 8274 add("param", "name value"); 8275 add("map", "name", flowContent, "area"); 8276 add("area", "alt coords shape href target rel media hreflang type"); 8277 add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : "")); 8278 add("colgroup", "span", "col"); 8279 add("col", "span"); 8280 add("tbody thead tfoot", "", "tr"); 8281 add("tr", "", "td th"); 8282 add("td", "colspan rowspan headers", flowContent); 8283 add("th", "colspan rowspan headers scope abbr", flowContent); 8284 add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent); 8285 add("fieldset", "disabled form name", flowContent, "legend"); 8286 add("label", "form for", phrasingContent); 8287 add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " + 8288 "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width" 8289 ); 8290 add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value", 8291 type == "html4" ? flowContent : phrasingContent); 8292 add("select", "disabled form multiple name required size", "option optgroup"); 8293 add("optgroup", "disabled label", "option"); 8294 add("option", "disabled label selected value"); 8295 add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap"); 8296 add("menu", "type label", flowContent, "li"); 8297 add("noscript", "", flowContent); 8298 8299 // Extend with HTML5 elements 8300 if (type != "html4") { 8301 add("wbr"); 8302 add("ruby", "", phrasingContent, "rt rp"); 8303 add("figcaption", "", flowContent); 8304 add("mark rt rp summary bdi", "", phrasingContent); 8305 add("canvas", "width height", flowContent); 8306 add("video", "src crossorigin poster preload autoplay mediagroup loop " + 8307 "muted controls width height buffered", flowContent, "track source"); 8308 add("audio", "src crossorigin preload autoplay mediagroup loop muted controls buffered volume", flowContent, "track source"); 8309 add("source", "src type media"); 8310 add("track", "kind src srclang label default"); 8311 add("datalist", "", phrasingContent, "option"); 8312 add("article section nav aside header footer", "", flowContent); 8313 add("hgroup", "", "h1 h2 h3 h4 h5 h6"); 8314 add("figure", "", flowContent, "figcaption"); 8315 add("time", "datetime", phrasingContent); 8316 add("dialog", "open", flowContent); 8317 add("command", "type label icon disabled checked radiogroup command"); 8318 add("output", "for form name", phrasingContent); 8319 add("progress", "value max", phrasingContent); 8320 add("meter", "value min max low high optimum", phrasingContent); 8321 add("details", "open", flowContent, "summary"); 8322 add("keygen", "autofocus challenge disabled form keytype name"); 8323 } 8324 8325 // Extend with HTML4 attributes unless it's html5-strict 8326 if (type != "html5-strict") { 8327 addAttrs("script", "language xml:space"); 8328 addAttrs("style", "xml:space"); 8329 addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace"); 8330 addAttrs("embed", "align name hspace vspace"); 8331 addAttrs("param", "valuetype type"); 8332 addAttrs("a", "charset name rev shape coords"); 8333 addAttrs("br", "clear"); 8334 addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); 8335 addAttrs("img", "name longdesc align border hspace vspace"); 8336 addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align"); 8337 addAttrs("font basefont", "size color face"); 8338 addAttrs("input", "usemap align"); 8339 addAttrs("select", "onchange"); 8340 addAttrs("textarea"); 8341 addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align"); 8342 addAttrs("ul", "type compact"); 8343 addAttrs("li", "type"); 8344 addAttrs("ol dl menu dir", "compact"); 8345 addAttrs("pre", "width xml:space"); 8346 addAttrs("hr", "align noshade size width"); 8347 addAttrs("isindex", "prompt"); 8348 addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor"); 8349 addAttrs("col", "width align char charoff valign"); 8350 addAttrs("colgroup", "width align char charoff valign"); 8351 addAttrs("thead", "align char charoff valign"); 8352 addAttrs("tr", "align char charoff valign bgcolor"); 8353 addAttrs("th", "axis align char charoff valign nowrap bgcolor width height"); 8354 addAttrs("form", "accept"); 8355 addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height"); 8356 addAttrs("tfoot", "align char charoff valign"); 8357 addAttrs("tbody", "align char charoff valign"); 8358 addAttrs("area", "nohref"); 8359 addAttrs("body", "background bgcolor text link vlink alink"); 8360 } 8361 8362 // Extend with HTML5 attributes unless it's html4 8363 if (type != "html4") { 8364 addAttrs("input button select textarea", "autofocus"); 8365 addAttrs("input textarea", "placeholder"); 8366 addAttrs("a", "download"); 8367 addAttrs("link script img", "crossorigin"); 8368 addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc 8369 } 8370 8371 // Special: iframe, ruby, video, audio, label 8372 8373 // Delete children of the same name from it's parent 8374 // For example: form can't have a child of the name form 8375 each(split('a form meter progress dfn'), function(name) { 8376 if (schema[name]) { 8377 delete schema[name].children[name]; 8378 } 8379 }); 8380 8381 // Delete header, footer, sectioning and heading content descendants 8382 /*each('dt th address', function(name) { 8383 delete schema[name].children[name]; 8384 });*/ 8385 8386 // Caption can't have tables 8387 delete schema.caption.children.table; 8388 8389 // TODO: LI:s can only have value if parent is OL 8390 8391 // TODO: Handle transparent elements 8392 // a ins del canvas map 8393 8394 mapCache[type] = schema; 8395 8396 return schema; 8397 } 8398 8399 function compileElementMap(value, mode) { 8400 var styles; 8401 8402 if (value) { 8403 styles = {}; 8404 8405 if (typeof value == 'string') { 8406 value = { 8407 '*': value 8408 }; 8409 } 8410 8411 // Convert styles into a rule list 8412 each(value, function(value, key) { 8413 styles[key] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/); 8414 }); 8415 } 8416 8417 return styles; 8418 } 8419 8420 /** 8421 * Constructs a new Schema instance. 8422 * 8423 * @constructor 8424 * @method Schema 8425 * @param {Object} settings Name/value settings object. 8426 */ 8427 return function(settings) { 8428 var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems; 8429 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses; 8430 var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, textInlineElementsMap; 8431 var customElementsMap = {}, specialElements = {}; 8432 8433 // Creates an lookup table map object for the specified option or the default value 8434 function createLookupTable(option, default_value, extendWith) { 8435 var value = settings[option]; 8436 8437 if (!value) { 8438 // Get cached default map or make it if needed 8439 value = mapCache[option]; 8440 8441 if (!value) { 8442 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); 8443 value = extend(value, extendWith); 8444 8445 mapCache[option] = value; 8446 } 8447 } else { 8448 // Create custom map 8449 value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); 8450 } 8451 8452 return value; 8453 } 8454 8455 settings = settings || {}; 8456 schemaItems = compileSchema(settings.schema); 8457 8458 // Allow all elements and attributes if verify_html is set to false 8459 if (settings.verify_html === false) { 8460 settings.valid_elements = '*[*]'; 8461 } 8462 8463 validStyles = compileElementMap(settings.valid_styles); 8464 invalidStyles = compileElementMap(settings.invalid_styles, 'map'); 8465 validClasses = compileElementMap(settings.valid_classes, 'map'); 8466 8467 // Setup map objects 8468 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object'); 8469 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 8470 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + 8471 'meta param embed source wbr track'); 8472 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + 8473 'noshade nowrap readonly selected autoplay loop controls'); 8474 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap); 8475 textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + 8476 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); 8477 blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + 8478 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + 8479 'datalist select optgroup', textBlockElementsMap); 8480 textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' + 8481 'dfn code mark q sup sub samp'); 8482 8483 each((settings.special || 'script noscript style textarea').split(' '), function(name) { 8484 specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi'); 8485 }); 8486 8487 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 8488 function patternToRegExp(str) { 8489 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 8490 } 8491 8492 // Parses the specified valid_elements string and adds to the current rules 8493 // This function is a bit hard to read since it's heavily optimized for speed 8494 function addValidElements(validElements) { 8495 var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, 8496 prefix, outputName, globalAttributes, globalAttributesOrder, key, value, 8497 elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/, 8498 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, 8499 hasPatternsRegExp = /[*?+]/; 8500 8501 if (validElements) { 8502 // Split valid elements into an array with rules 8503 validElements = split(validElements, ','); 8504 8505 if (elements['@']) { 8506 globalAttributes = elements['@'].attributes; 8507 globalAttributesOrder = elements['@'].attributesOrder; 8508 } 8509 8510 // Loop all rules 8511 for (ei = 0, el = validElements.length; ei < el; ei++) { 8512 // Parse element rule 8513 matches = elementRuleRegExp.exec(validElements[ei]); 8514 if (matches) { 8515 // Setup local names for matches 8516 prefix = matches[1]; 8517 elementName = matches[2]; 8518 outputName = matches[3]; 8519 attrData = matches[5]; 8520 8521 // Create new attributes and attributesOrder 8522 attributes = {}; 8523 attributesOrder = []; 8524 8525 // Create the new element 8526 element = { 8527 attributes: attributes, 8528 attributesOrder: attributesOrder 8529 }; 8530 8531 // Padd empty elements prefix 8532 if (prefix === '#') { 8533 element.paddEmpty = true; 8534 } 8535 8536 // Remove empty elements prefix 8537 if (prefix === '-') { 8538 element.removeEmpty = true; 8539 } 8540 8541 if (matches[4] === '!') { 8542 element.removeEmptyAttrs = true; 8543 } 8544 8545 // Copy attributes from global rule into current rule 8546 if (globalAttributes) { 8547 for (key in globalAttributes) { 8548 attributes[key] = globalAttributes[key]; 8549 } 8550 8551 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); 8552 } 8553 8554 // Attributes defined 8555 if (attrData) { 8556 attrData = split(attrData, '|'); 8557 for (ai = 0, al = attrData.length; ai < al; ai++) { 8558 matches = attrRuleRegExp.exec(attrData[ai]); 8559 if (matches) { 8560 attr = {}; 8561 attrType = matches[1]; 8562 attrName = matches[2].replace(/::/g, ':'); 8563 prefix = matches[3]; 8564 value = matches[4]; 8565 8566 // Required 8567 if (attrType === '!') { 8568 element.attributesRequired = element.attributesRequired || []; 8569 element.attributesRequired.push(attrName); 8570 attr.required = true; 8571 } 8572 8573 // Denied from global 8574 if (attrType === '-') { 8575 delete attributes[attrName]; 8576 attributesOrder.splice(inArray(attributesOrder, attrName), 1); 8577 continue; 8578 } 8579 8580 // Default value 8581 if (prefix) { 8582 // Default value 8583 if (prefix === '=') { 8584 element.attributesDefault = element.attributesDefault || []; 8585 element.attributesDefault.push({name: attrName, value: value}); 8586 attr.defaultValue = value; 8587 } 8588 8589 // Forced value 8590 if (prefix === ':') { 8591 element.attributesForced = element.attributesForced || []; 8592 element.attributesForced.push({name: attrName, value: value}); 8593 attr.forcedValue = value; 8594 } 8595 8596 // Required values 8597 if (prefix === '<') { 8598 attr.validValues = makeMap(value, '?'); 8599 } 8600 } 8601 8602 // Check for attribute patterns 8603 if (hasPatternsRegExp.test(attrName)) { 8604 element.attributePatterns = element.attributePatterns || []; 8605 attr.pattern = patternToRegExp(attrName); 8606 element.attributePatterns.push(attr); 8607 } else { 8608 // Add attribute to order list if it doesn't already exist 8609 if (!attributes[attrName]) { 8610 attributesOrder.push(attrName); 8611 } 8612 8613 attributes[attrName] = attr; 8614 } 8615 } 8616 } 8617 } 8618 8619 // Global rule, store away these for later usage 8620 if (!globalAttributes && elementName == '@') { 8621 globalAttributes = attributes; 8622 globalAttributesOrder = attributesOrder; 8623 } 8624 8625 // Handle substitute elements such as b/strong 8626 if (outputName) { 8627 element.outputName = elementName; 8628 elements[outputName] = element; 8629 } 8630 8631 // Add pattern or exact element 8632 if (hasPatternsRegExp.test(elementName)) { 8633 element.pattern = patternToRegExp(elementName); 8634 patternElements.push(element); 8635 } else { 8636 elements[elementName] = element; 8637 } 8638 } 8639 } 8640 } 8641 } 8642 8643 function setValidElements(validElements) { 8644 elements = {}; 8645 patternElements = []; 8646 8647 addValidElements(validElements); 8648 8649 each(schemaItems, function(element, name) { 8650 children[name] = element.children; 8651 }); 8652 } 8653 8654 // Adds custom non HTML elements to the schema 8655 function addCustomElements(customElements) { 8656 var customElementRegExp = /^(~)?(.+)$/; 8657 8658 if (customElements) { 8659 // Flush cached items since we are altering the default maps 8660 mapCache.text_block_elements = mapCache.block_elements = null; 8661 8662 each(split(customElements, ','), function(rule) { 8663 var matches = customElementRegExp.exec(rule), 8664 inline = matches[1] === '~', 8665 cloneName = inline ? 'span' : 'div', 8666 name = matches[2]; 8667 8668 children[name] = children[cloneName]; 8669 customElementsMap[name] = cloneName; 8670 8671 // If it's not marked as inline then add it to valid block elements 8672 if (!inline) { 8673 blockElementsMap[name.toUpperCase()] = {}; 8674 blockElementsMap[name] = {}; 8675 } 8676 8677 // Add elements clone if needed 8678 if (!elements[name]) { 8679 var customRule = elements[cloneName]; 8680 8681 customRule = extend({}, customRule); 8682 delete customRule.removeEmptyAttrs; 8683 delete customRule.removeEmpty; 8684 8685 elements[name] = customRule; 8686 } 8687 8688 // Add custom elements at span/div positions 8689 each(children, function(element, elmName) { 8690 if (element[cloneName]) { 8691 children[elmName] = element = extend({}, children[elmName]); 8692 element[name] = element[cloneName]; 8693 } 8694 }); 8695 }); 8696 } 8697 } 8698 8699 // Adds valid children to the schema object 8700 function addValidChildren(validChildren) { 8701 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; 8702 8703 if (validChildren) { 8704 each(split(validChildren, ','), function(rule) { 8705 var matches = childRuleRegExp.exec(rule), parent, prefix; 8706 8707 if (matches) { 8708 prefix = matches[1]; 8709 8710 // Add/remove items from default 8711 if (prefix) { 8712 parent = children[matches[2]]; 8713 } else { 8714 parent = children[matches[2]] = {'#comment': {}}; 8715 } 8716 8717 parent = children[matches[2]]; 8718 8719 each(split(matches[3], '|'), function(child) { 8720 if (prefix === '-') { 8721 // Clone the element before we delete 8722 // things in it to not mess up default schemas 8723 children[matches[2]] = parent = extend({}, children[matches[2]]); 8724 8725 delete parent[child]; 8726 } else { 8727 parent[child] = {}; 8728 } 8729 }); 8730 } 8731 }); 8732 } 8733 } 8734 8735 function getElementRule(name) { 8736 var element = elements[name], i; 8737 8738 // Exact match found 8739 if (element) { 8740 return element; 8741 } 8742 8743 // No exact match then try the patterns 8744 i = patternElements.length; 8745 while (i--) { 8746 element = patternElements[i]; 8747 8748 if (element.pattern.test(name)) { 8749 return element; 8750 } 8751 } 8752 } 8753 8754 if (!settings.valid_elements) { 8755 // No valid elements defined then clone the elements from the schema spec 8756 each(schemaItems, function(element, name) { 8757 elements[name] = { 8758 attributes: element.attributes, 8759 attributesOrder: element.attributesOrder 8760 }; 8761 8762 children[name] = element.children; 8763 }); 8764 8765 // Switch these on HTML4 8766 if (settings.schema != "html5") { 8767 each(split('strong/b em/i'), function(item) { 8768 item = split(item, '/'); 8769 elements[item[1]].outputName = item[0]; 8770 }); 8771 } 8772 8773 // Add default alt attribute for images 8774 elements.img.attributesDefault = [{name: 'alt', value: ''}]; 8775 8776 // Remove these if they are empty by default 8777 each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) { 8778 if (elements[name]) { 8779 elements[name].removeEmpty = true; 8780 } 8781 }); 8782 8783 // Padd these by default 8784 each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) { 8785 elements[name].paddEmpty = true; 8786 }); 8787 8788 // Remove these if they have no attributes 8789 each(split('span'), function(name) { 8790 elements[name].removeEmptyAttrs = true; 8791 }); 8792 8793 // Remove these by default 8794 // TODO: Reenable in 4.1 8795 /*each(split('script style'), function(name) { 8796 delete elements[name]; 8797 });*/ 8798 } else { 8799 setValidElements(settings.valid_elements); 8800 } 8801 8802 addCustomElements(settings.custom_elements); 8803 addValidChildren(settings.valid_children); 8804 addValidElements(settings.extended_valid_elements); 8805 8806 // Todo: Remove this when we fix list handling to be valid 8807 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 8808 8809 // Delete invalid elements 8810 if (settings.invalid_elements) { 8811 each(explode(settings.invalid_elements), function(item) { 8812 if (elements[item]) { 8813 delete elements[item]; 8814 } 8815 }); 8816 } 8817 8818 // If the user didn't allow span only allow internal spans 8819 if (!getElementRule('span')) { 8820 addValidElements('span[!data-mce-type|*]'); 8821 } 8822 8823 /** 8824 * Name/value map object with valid parents and children to those parents. 8825 * 8826 * @example 8827 * children = { 8828 * div:{p:{}, h1:{}} 8829 * }; 8830 * @field children 8831 * @type Object 8832 */ 8833 self.children = children; 8834 8835 /** 8836 * Name/value map object with valid styles for each element. 8837 * 8838 * @method getValidStyles 8839 * @type Object 8840 */ 8841 self.getValidStyles = function() { 8842 return validStyles; 8843 }; 8844 8845 /** 8846 * Name/value map object with valid styles for each element. 8847 * 8848 * @method getInvalidStyles 8849 * @type Object 8850 */ 8851 self.getInvalidStyles = function() { 8852 return invalidStyles; 8853 }; 8854 8855 /** 8856 * Name/value map object with valid classes for each element. 8857 * 8858 * @method getValidClasses 8859 * @type Object 8860 */ 8861 self.getValidClasses = function() { 8862 return validClasses; 8863 }; 8864 8865 /** 8866 * Returns a map with boolean attributes. 8867 * 8868 * @method getBoolAttrs 8869 * @return {Object} Name/value lookup map for boolean attributes. 8870 */ 8871 self.getBoolAttrs = function() { 8872 return boolAttrMap; 8873 }; 8874 8875 /** 8876 * Returns a map with block elements. 8877 * 8878 * @method getBlockElements 8879 * @return {Object} Name/value lookup map for block elements. 8880 */ 8881 self.getBlockElements = function() { 8882 return blockElementsMap; 8883 }; 8884 8885 /** 8886 * Returns a map with text block elements. Such as: p,h1-h6,div,address 8887 * 8888 * @method getTextBlockElements 8889 * @return {Object} Name/value lookup map for block elements. 8890 */ 8891 self.getTextBlockElements = function() { 8892 return textBlockElementsMap; 8893 }; 8894 8895 /** 8896 * Returns a map of inline text format nodes for example strong/span or ins. 8897 * 8898 * @method getTextInlineElements 8899 * @return {Object} Name/value lookup map for text format elements. 8900 */ 8901 self.getTextInlineElements = function() { 8902 return textInlineElementsMap; 8903 }; 8904 8905 /** 8906 * Returns a map with short ended elements such as BR or IMG. 8907 * 8908 * @method getShortEndedElements 8909 * @return {Object} Name/value lookup map for short ended elements. 8910 */ 8911 self.getShortEndedElements = function() { 8912 return shortEndedElementsMap; 8913 }; 8914 8915 /** 8916 * Returns a map with self closing tags such as <li>. 8917 * 8918 * @method getSelfClosingElements 8919 * @return {Object} Name/value lookup map for self closing tags elements. 8920 */ 8921 self.getSelfClosingElements = function() { 8922 return selfClosingElementsMap; 8923 }; 8924 8925 /** 8926 * Returns a map with elements that should be treated as contents regardless if it has text 8927 * content in them or not such as TD, VIDEO or IMG. 8928 * 8929 * @method getNonEmptyElements 8930 * @return {Object} Name/value lookup map for non empty elements. 8931 */ 8932 self.getNonEmptyElements = function() { 8933 return nonEmptyElementsMap; 8934 }; 8935 8936 /** 8937 * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. 8938 * 8939 * @method getWhiteSpaceElements 8940 * @return {Object} Name/value lookup map for white space elements. 8941 */ 8942 self.getWhiteSpaceElements = function() { 8943 return whiteSpaceElementsMap; 8944 }; 8945 8946 /** 8947 * Returns a map with special elements. These are elements that needs to be parsed 8948 * in a special way such as script, style, textarea etc. The map object values 8949 * are regexps used to find the end of the element. 8950 * 8951 * @method getSpecialElements 8952 * @return {Object} Name/value lookup map for special elements. 8953 */ 8954 self.getSpecialElements = function() { 8955 return specialElements; 8956 }; 8957 8958 /** 8959 * Returns true/false if the specified element and it's child is valid or not 8960 * according to the schema. 8961 * 8962 * @method isValidChild 8963 * @param {String} name Element name to check for. 8964 * @param {String} child Element child to verify. 8965 * @return {Boolean} True/false if the element is a valid child of the specified parent. 8966 */ 8967 self.isValidChild = function(name, child) { 8968 var parent = children[name]; 8969 8970 return !!(parent && parent[child]); 8971 }; 8972 8973 /** 8974 * Returns true/false if the specified element name and optional attribute is 8975 * valid according to the schema. 8976 * 8977 * @method isValid 8978 * @param {String} name Name of element to check. 8979 * @param {String} attr Optional attribute name to check for. 8980 * @return {Boolean} True/false if the element and attribute is valid. 8981 */ 8982 self.isValid = function(name, attr) { 8983 var attrPatterns, i, rule = getElementRule(name); 8984 8985 // Check if it's a valid element 8986 if (rule) { 8987 if (attr) { 8988 // Check if attribute name exists 8989 if (rule.attributes[attr]) { 8990 return true; 8991 } 8992 8993 // Check if attribute matches a regexp pattern 8994 attrPatterns = rule.attributePatterns; 8995 if (attrPatterns) { 8996 i = attrPatterns.length; 8997 while (i--) { 8998 if (attrPatterns[i].pattern.test(name)) { 8999 return true; 9000 } 9001 } 9002 } 9003 } else { 9004 return true; 9005 } 9006 } 9007 9008 // No match 9009 return false; 9010 }; 9011 9012 /** 9013 * Returns true/false if the specified element is valid or not 9014 * according to the schema. 9015 * 9016 * @method getElementRule 9017 * @param {String} name Element name to check for. 9018 * @return {Object} Element object or undefined if the element isn't valid. 9019 */ 9020 self.getElementRule = getElementRule; 9021 9022 /** 9023 * Returns an map object of all custom elements. 9024 * 9025 * @method getCustomElements 9026 * @return {Object} Name/value map object of all custom elements. 9027 */ 9028 self.getCustomElements = function() { 9029 return customElementsMap; 9030 }; 9031 9032 /** 9033 * Parses a valid elements string and adds it to the schema. The valid elements 9034 * format is for example "element[attr=default|otherattr]". 9035 * Existing rules will be replaced with the ones specified, so this extends the schema. 9036 * 9037 * @method addValidElements 9038 * @param {String} valid_elements String in the valid elements format to be parsed. 9039 */ 9040 self.addValidElements = addValidElements; 9041 9042 /** 9043 * Parses a valid elements string and sets it to the schema. The valid elements 9044 * format is for example "element[attr=default|otherattr]". 9045 * Existing rules will be replaced with the ones specified, so this extends the schema. 9046 * 9047 * @method setValidElements 9048 * @param {String} valid_elements String in the valid elements format to be parsed. 9049 */ 9050 self.setValidElements = setValidElements; 9051 9052 /** 9053 * Adds custom non HTML elements to the schema. 9054 * 9055 * @method addCustomElements 9056 * @param {String} custom_elements Comma separated list of custom elements to add. 9057 */ 9058 self.addCustomElements = addCustomElements; 9059 9060 /** 9061 * Parses a valid children string and adds them to the schema structure. The valid children 9062 * format is for example: "element[child1|child2]". 9063 * 9064 * @method addValidChildren 9065 * @param {String} valid_children Valid children elements string to parse 9066 */ 9067 self.addValidChildren = addValidChildren; 9068 9069 self.elements = elements; 9070 }; 9071 }); 9072 9073 // Included from: js/tinymce/classes/html/SaxParser.js 9074 9075 /** 9076 * SaxParser.js 9077 * 9078 * Copyright, Moxiecode Systems AB 9079 * Released under LGPL License. 9080 * 9081 * License: http://www.tinymce.com/license 9082 * Contributing: http://www.tinymce.com/contributing 9083 */ 9084 9085 /*eslint max-depth:[2, 9] */ 9086 9087 /** 9088 * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will 9089 * always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements 9090 * and attributes that doesn't fit the schema if the validate setting is enabled. 9091 * 9092 * @example 9093 * var parser = new tinymce.html.SaxParser({ 9094 * validate: true, 9095 * 9096 * comment: function(text) { 9097 * console.log('Comment:', text); 9098 * }, 9099 * 9100 * cdata: function(text) { 9101 * console.log('CDATA:', text); 9102 * }, 9103 * 9104 * text: function(text, raw) { 9105 * console.log('Text:', text, 'Raw:', raw); 9106 * }, 9107 * 9108 * start: function(name, attrs, empty) { 9109 * console.log('Start:', name, attrs, empty); 9110 * }, 9111 * 9112 * end: function(name) { 9113 * console.log('End:', name); 9114 * }, 9115 * 9116 * pi: function(name, text) { 9117 * console.log('PI:', name, text); 9118 * }, 9119 * 9120 * doctype: function(text) { 9121 * console.log('DocType:', text); 9122 * } 9123 * }, schema); 9124 * @class tinymce.html.SaxParser 9125 * @version 3.4 9126 */ 9127 define("tinymce/html/SaxParser", [ 9128 "tinymce/html/Schema", 9129 "tinymce/html/Entities", 9130 "tinymce/util/Tools" 9131 ], function(Schema, Entities, Tools) { 9132 var each = Tools.each; 9133 9134 /** 9135 * Returns the index of the end tag for a specific start tag. This can be 9136 * used to skip all children of a parent element from being processed. 9137 * 9138 * @private 9139 * @method findEndTag 9140 * @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements. 9141 * @param {String} html HTML string to find the end tag in. 9142 * @param {Number} startIndex Indext to start searching at should be after the start tag. 9143 * @return {Number} Index of the end tag. 9144 */ 9145 function findEndTag(schema, html, startIndex) { 9146 var count = 1, index, matches, tokenRegExp, shortEndedElements; 9147 9148 shortEndedElements = schema.getShortEndedElements(); 9149 tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g; 9150 tokenRegExp.lastIndex = index = startIndex; 9151 9152 while ((matches = tokenRegExp.exec(html))) { 9153 index = tokenRegExp.lastIndex; 9154 9155 if (matches[1] === '/') { // End element 9156 count--; 9157 } else if (!matches[1]) { // Start element 9158 if (matches[2] in shortEndedElements) { 9159 continue; 9160 } 9161 9162 count++; 9163 } 9164 9165 if (count === 0) { 9166 break; 9167 } 9168 } 9169 9170 return index; 9171 } 9172 9173 /** 9174 * Constructs a new SaxParser instance. 9175 * 9176 * @constructor 9177 * @method SaxParser 9178 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. 9179 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. 9180 */ 9181 function SaxParser(settings, schema) { 9182 var self = this; 9183 9184 function noop() {} 9185 9186 settings = settings || {}; 9187 self.schema = schema = schema || new Schema(); 9188 9189 if (settings.fix_self_closing !== false) { 9190 settings.fix_self_closing = true; 9191 } 9192 9193 // Add handler functions from settings and setup default handlers 9194 each('comment cdata text start end pi doctype'.split(' '), function(name) { 9195 if (name) { 9196 self[name] = settings[name] || noop; 9197 } 9198 }); 9199 9200 /** 9201 * Parses the specified HTML string and executes the callbacks for each item it finds. 9202 * 9203 * @example 9204 * new SaxParser({...}).parse('<b>text</b>'); 9205 * @method parse 9206 * @param {String} html Html string to sax parse. 9207 */ 9208 self.parse = function(html) { 9209 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name; 9210 var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded; 9211 var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns; 9212 var attributesRequired, attributesDefault, attributesForced; 9213 var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0; 9214 var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster'); 9215 var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i; 9216 9217 function processEndTag(name) { 9218 var pos, i; 9219 9220 // Find position of parent of the same type 9221 pos = stack.length; 9222 while (pos--) { 9223 if (stack[pos].name === name) { 9224 break; 9225 } 9226 } 9227 9228 // Found parent 9229 if (pos >= 0) { 9230 // Close all the open elements 9231 for (i = stack.length - 1; i >= pos; i--) { 9232 name = stack[i]; 9233 9234 if (name.valid) { 9235 self.end(name.name); 9236 } 9237 } 9238 9239 // Remove the open elements from the stack 9240 stack.length = pos; 9241 } 9242 } 9243 9244 function parseAttribute(match, name, value, val2, val3) { 9245 var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g; 9246 9247 name = name.toLowerCase(); 9248 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute 9249 9250 // Validate name and value pass through all data- attributes 9251 if (validate && !isInternalElement && name.indexOf('data-') !== 0) { 9252 attrRule = validAttributesMap[name]; 9253 9254 // Find rule by pattern matching 9255 if (!attrRule && validAttributePatterns) { 9256 i = validAttributePatterns.length; 9257 while (i--) { 9258 attrRule = validAttributePatterns[i]; 9259 if (attrRule.pattern.test(name)) { 9260 break; 9261 } 9262 } 9263 9264 // No rule matched 9265 if (i === -1) { 9266 attrRule = null; 9267 } 9268 } 9269 9270 // No attribute rule found 9271 if (!attrRule) { 9272 return; 9273 } 9274 9275 // Validate value 9276 if (attrRule.validValues && !(value in attrRule.validValues)) { 9277 return; 9278 } 9279 } 9280 9281 // Block any javascript: urls or non image data uris 9282 if (filteredUrlAttrs[name] && !settings.allow_script_urls) { 9283 var uri = value.replace(trimRegExp, ''); 9284 9285 try { 9286 // Might throw malformed URI sequence 9287 uri = decodeURIComponent(uri); 9288 } catch (ex) { 9289 // Fallback to non UTF-8 decoder 9290 uri = unescape(uri); 9291 } 9292 9293 if (scriptUriRegExp.test(uri)) { 9294 return; 9295 } 9296 9297 if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) { 9298 return; 9299 } 9300 } 9301 9302 // Add attribute to list and map 9303 attrList.map[name] = value; 9304 attrList.push({ 9305 name: name, 9306 value: value 9307 }); 9308 } 9309 9310 // Precompile RegExps and map objects 9311 tokenRegExp = new RegExp('<(?:' + 9312 '(?:!--([\\w\\W]*?)-->)|' + // Comment 9313 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA 9314 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE 9315 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI 9316 '(?:\\/([^>]+)>)|' + // End element 9317 '(?:([A-Za-z0-9\\-_\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element 9318 ')', 'g'); 9319 9320 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; 9321 9322 // Setup lookup tables for empty elements and boolean attributes 9323 shortEndedElements = schema.getShortEndedElements(); 9324 selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); 9325 fillAttrsMap = schema.getBoolAttrs(); 9326 validate = settings.validate; 9327 removeInternalElements = settings.remove_internals; 9328 fixSelfClosing = settings.fix_self_closing; 9329 specialElements = schema.getSpecialElements(); 9330 9331 while ((matches = tokenRegExp.exec(html))) { 9332 // Text 9333 if (index < matches.index) { 9334 self.text(decode(html.substr(index, matches.index - index))); 9335 } 9336 9337 if ((value = matches[6])) { // End element 9338 value = value.toLowerCase(); 9339 9340 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 9341 if (value.charAt(0) === ':') { 9342 value = value.substr(1); 9343 } 9344 9345 processEndTag(value); 9346 } else if ((value = matches[7])) { // Start element 9347 value = value.toLowerCase(); 9348 9349 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 9350 if (value.charAt(0) === ':') { 9351 value = value.substr(1); 9352 } 9353 9354 isShortEnded = value in shortEndedElements; 9355 9356 // Is self closing tag for example an <li> after an open <li> 9357 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) { 9358 processEndTag(value); 9359 } 9360 9361 // Validate element 9362 if (!validate || (elementRule = schema.getElementRule(value))) { 9363 isValidElement = true; 9364 9365 // Grab attributes map and patters when validation is enabled 9366 if (validate) { 9367 validAttributesMap = elementRule.attributes; 9368 validAttributePatterns = elementRule.attributePatterns; 9369 } 9370 9371 // Parse attributes 9372 if ((attribsValue = matches[8])) { 9373 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element 9374 9375 // If the element has internal attributes then remove it if we are told to do so 9376 if (isInternalElement && removeInternalElements) { 9377 isValidElement = false; 9378 } 9379 9380 attrList = []; 9381 attrList.map = {}; 9382 9383 attribsValue.replace(attrRegExp, parseAttribute); 9384 } else { 9385 attrList = []; 9386 attrList.map = {}; 9387 } 9388 9389 // Process attributes if validation is enabled 9390 if (validate && !isInternalElement) { 9391 attributesRequired = elementRule.attributesRequired; 9392 attributesDefault = elementRule.attributesDefault; 9393 attributesForced = elementRule.attributesForced; 9394 anyAttributesRequired = elementRule.removeEmptyAttrs; 9395 9396 // Check if any attribute exists 9397 if (anyAttributesRequired && !attrList.length) { 9398 isValidElement = false; 9399 } 9400 9401 // Handle forced attributes 9402 if (attributesForced) { 9403 i = attributesForced.length; 9404 while (i--) { 9405 attr = attributesForced[i]; 9406 name = attr.name; 9407 attrValue = attr.value; 9408 9409 if (attrValue === '{$uid}') { 9410 attrValue = 'mce_' + idCount++; 9411 } 9412 9413 attrList.map[name] = attrValue; 9414 attrList.push({name: name, value: attrValue}); 9415 } 9416 } 9417 9418 // Handle default attributes 9419 if (attributesDefault) { 9420 i = attributesDefault.length; 9421 while (i--) { 9422 attr = attributesDefault[i]; 9423 name = attr.name; 9424 9425 if (!(name in attrList.map)) { 9426 attrValue = attr.value; 9427 9428 if (attrValue === '{$uid}') { 9429 attrValue = 'mce_' + idCount++; 9430 } 9431 9432 attrList.map[name] = attrValue; 9433 attrList.push({name: name, value: attrValue}); 9434 } 9435 } 9436 } 9437 9438 // Handle required attributes 9439 if (attributesRequired) { 9440 i = attributesRequired.length; 9441 while (i--) { 9442 if (attributesRequired[i] in attrList.map) { 9443 break; 9444 } 9445 } 9446 9447 // None of the required attributes where found 9448 if (i === -1) { 9449 isValidElement = false; 9450 } 9451 } 9452 9453 // Invalidate element if it's marked as bogus 9454 if ((attr = attrList.map['data-mce-bogus'])) { 9455 if (attr === 'all') { 9456 index = findEndTag(schema, html, tokenRegExp.lastIndex); 9457 tokenRegExp.lastIndex = index; 9458 continue; 9459 } 9460 9461 isValidElement = false; 9462 } 9463 } 9464 9465 if (isValidElement) { 9466 self.start(value, attrList, isShortEnded); 9467 } 9468 } else { 9469 isValidElement = false; 9470 } 9471 9472 // Treat script, noscript and style a bit different since they may include code that looks like elements 9473 if ((endRegExp = specialElements[value])) { 9474 endRegExp.lastIndex = index = matches.index + matches[0].length; 9475 9476 if ((matches = endRegExp.exec(html))) { 9477 if (isValidElement) { 9478 text = html.substr(index, matches.index - index); 9479 } 9480 9481 index = matches.index + matches[0].length; 9482 } else { 9483 text = html.substr(index); 9484 index = html.length; 9485 } 9486 9487 if (isValidElement) { 9488 if (text.length > 0) { 9489 self.text(text, true); 9490 } 9491 9492 self.end(value); 9493 } 9494 9495 tokenRegExp.lastIndex = index; 9496 continue; 9497 } 9498 9499 // Push value on to stack 9500 if (!isShortEnded) { 9501 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) { 9502 stack.push({name: value, valid: isValidElement}); 9503 } else if (isValidElement) { 9504 self.end(value); 9505 } 9506 } 9507 } else if ((value = matches[1])) { // Comment 9508 // Padd comment value to avoid browsers from parsing invalid comments as HTML 9509 if (value.charAt(0) === '>') { 9510 value = ' ' + value; 9511 } 9512 9513 if (!settings.allow_conditional_comments && value.substr(0, 3) === '[if') { 9514 value = ' ' + value; 9515 } 9516 9517 self.comment(value); 9518 } else if ((value = matches[2])) { // CDATA 9519 self.cdata(value); 9520 } else if ((value = matches[3])) { // DOCTYPE 9521 self.doctype(value); 9522 } else if ((value = matches[4])) { // PI 9523 self.pi(value, matches[5]); 9524 } 9525 9526 index = matches.index + matches[0].length; 9527 } 9528 9529 // Text 9530 if (index < html.length) { 9531 self.text(decode(html.substr(index))); 9532 } 9533 9534 // Close any open elements 9535 for (i = stack.length - 1; i >= 0; i--) { 9536 value = stack[i]; 9537 9538 if (value.valid) { 9539 self.end(value.name); 9540 } 9541 } 9542 }; 9543 } 9544 9545 SaxParser.findEndTag = findEndTag; 9546 9547 return SaxParser; 9548 }); 9549 9550 // Included from: js/tinymce/classes/html/DomParser.js 9551 9552 /** 9553 * DomParser.js 9554 * 9555 * Copyright, Moxiecode Systems AB 9556 * Released under LGPL License. 9557 * 9558 * License: http://www.tinymce.com/license 9559 * Contributing: http://www.tinymce.com/contributing 9560 */ 9561 9562 /** 9563 * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make 9564 * sure that the node tree is valid according to the specified schema. 9565 * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p> 9566 * 9567 * @example 9568 * var parser = new tinymce.html.DomParser({validate: true}, schema); 9569 * var rootNode = parser.parse('<h1>content</h1>'); 9570 * 9571 * @class tinymce.html.DomParser 9572 * @version 3.4 9573 */ 9574 define("tinymce/html/DomParser", [ 9575 "tinymce/html/Node", 9576 "tinymce/html/Schema", 9577 "tinymce/html/SaxParser", 9578 "tinymce/util/Tools" 9579 ], function(Node, Schema, SaxParser, Tools) { 9580 var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend; 9581 9582 /** 9583 * Constructs a new DomParser instance. 9584 * 9585 * @constructor 9586 * @method DomParser 9587 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. 9588 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. 9589 */ 9590 return function(settings, schema) { 9591 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; 9592 9593 settings = settings || {}; 9594 settings.validate = "validate" in settings ? settings.validate : true; 9595 settings.root_name = settings.root_name || 'body'; 9596 self.schema = schema = schema || new Schema(); 9597 9598 function fixInvalidChildren(nodes) { 9599 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i; 9600 var nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode; 9601 9602 nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table'); 9603 nonEmptyElements = schema.getNonEmptyElements(); 9604 textBlockElements = schema.getTextBlockElements(); 9605 9606 for (ni = 0; ni < nodes.length; ni++) { 9607 node = nodes[ni]; 9608 9609 // Already removed or fixed 9610 if (!node.parent || node.fixed) { 9611 continue; 9612 } 9613 9614 // If the invalid element is a text block and the text block is within a parent LI element 9615 // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office 9616 if (textBlockElements[node.name] && node.parent.name == 'li') { 9617 // Move sibling text blocks after LI element 9618 sibling = node.next; 9619 while (sibling) { 9620 if (textBlockElements[sibling.name]) { 9621 sibling.name = 'li'; 9622 sibling.fixed = true; 9623 node.parent.insert(sibling, node.parent); 9624 } else { 9625 break; 9626 } 9627 9628 sibling = sibling.next; 9629 } 9630 9631 // Unwrap current text block 9632 node.unwrap(node); 9633 continue; 9634 } 9635 9636 // Get list of all parent nodes until we find a valid parent to stick the child into 9637 parents = [node]; 9638 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && 9639 !nonSplitableElements[parent.name]; parent = parent.parent) { 9640 parents.push(parent); 9641 } 9642 9643 // Found a suitable parent 9644 if (parent && parents.length > 1) { 9645 // Reverse the array since it makes looping easier 9646 parents.reverse(); 9647 9648 // Clone the related parent and insert that after the moved node 9649 newParent = currentNode = self.filterNode(parents[0].clone()); 9650 9651 // Start cloning and moving children on the left side of the target node 9652 for (i = 0; i < parents.length - 1; i++) { 9653 if (schema.isValidChild(currentNode.name, parents[i].name)) { 9654 tempNode = self.filterNode(parents[i].clone()); 9655 currentNode.append(tempNode); 9656 } else { 9657 tempNode = currentNode; 9658 } 9659 9660 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) { 9661 nextNode = childNode.next; 9662 tempNode.append(childNode); 9663 childNode = nextNode; 9664 } 9665 9666 currentNode = tempNode; 9667 } 9668 9669 if (!newParent.isEmpty(nonEmptyElements)) { 9670 parent.insert(newParent, parents[0], true); 9671 parent.insert(node, newParent); 9672 } else { 9673 parent.insert(node, parents[0], true); 9674 } 9675 9676 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p> 9677 parent = parents[0]; 9678 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { 9679 parent.empty().remove(); 9680 } 9681 } else if (node.parent) { 9682 // If it's an LI try to find a UL/OL for it or wrap it 9683 if (node.name === 'li') { 9684 sibling = node.prev; 9685 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 9686 sibling.append(node); 9687 continue; 9688 } 9689 9690 sibling = node.next; 9691 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 9692 sibling.insert(node, sibling.firstChild, true); 9693 continue; 9694 } 9695 9696 node.wrap(self.filterNode(new Node('ul', 1))); 9697 continue; 9698 } 9699 9700 // Try wrapping the element in a DIV 9701 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 9702 node.wrap(self.filterNode(new Node('div', 1))); 9703 } else { 9704 // We failed wrapping it, then remove or unwrap it 9705 if (node.name === 'style' || node.name === 'script') { 9706 node.empty().remove(); 9707 } else { 9708 node.unwrap(); 9709 } 9710 } 9711 } 9712 } 9713 } 9714 9715 /** 9716 * Runs the specified node though the element and attributes filters. 9717 * 9718 * @method filterNode 9719 * @param {tinymce.html.Node} Node the node to run filters on. 9720 * @return {tinymce.html.Node} The passed in node. 9721 */ 9722 self.filterNode = function(node) { 9723 var i, name, list; 9724 9725 // Run element filters 9726 if (name in nodeFilters) { 9727 list = matchedNodes[name]; 9728 9729 if (list) { 9730 list.push(node); 9731 } else { 9732 matchedNodes[name] = [node]; 9733 } 9734 } 9735 9736 // Run attribute filters 9737 i = attributeFilters.length; 9738 while (i--) { 9739 name = attributeFilters[i].name; 9740 9741 if (name in node.attributes.map) { 9742 list = matchedAttributes[name]; 9743 9744 if (list) { 9745 list.push(node); 9746 } else { 9747 matchedAttributes[name] = [node]; 9748 } 9749 } 9750 } 9751 9752 return node; 9753 }; 9754 9755 /** 9756 * Adds a node filter function to the parser, the parser will collect the specified nodes by name 9757 * and then execute the callback ones it has finished parsing the document. 9758 * 9759 * @example 9760 * parser.addNodeFilter('p,h1', function(nodes, name) { 9761 * for (var i = 0; i < nodes.length; i++) { 9762 * console.log(nodes[i].name); 9763 * } 9764 * }); 9765 * @method addNodeFilter 9766 * @method {String} name Comma separated list of nodes to collect. 9767 * @param {function} callback Callback function to execute once it has collected nodes. 9768 */ 9769 self.addNodeFilter = function(name, callback) { 9770 each(explode(name), function(name) { 9771 var list = nodeFilters[name]; 9772 9773 if (!list) { 9774 nodeFilters[name] = list = []; 9775 } 9776 9777 list.push(callback); 9778 }); 9779 }; 9780 9781 /** 9782 * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes 9783 * and then execute the callback ones it has finished parsing the document. 9784 * 9785 * @example 9786 * parser.addAttributeFilter('src,href', function(nodes, name) { 9787 * for (var i = 0; i < nodes.length; i++) { 9788 * console.log(nodes[i].name); 9789 * } 9790 * }); 9791 * @method addAttributeFilter 9792 * @method {String} name Comma separated list of nodes to collect. 9793 * @param {function} callback Callback function to execute once it has collected nodes. 9794 */ 9795 self.addAttributeFilter = function(name, callback) { 9796 each(explode(name), function(name) { 9797 var i; 9798 9799 for (i = 0; i < attributeFilters.length; i++) { 9800 if (attributeFilters[i].name === name) { 9801 attributeFilters[i].callbacks.push(callback); 9802 return; 9803 } 9804 } 9805 9806 attributeFilters.push({name: name, callbacks: [callback]}); 9807 }); 9808 }; 9809 9810 /** 9811 * Parses the specified HTML string into a DOM like node tree and returns the result. 9812 * 9813 * @example 9814 * var rootNode = new DomParser({...}).parse('<b>text</b>'); 9815 * @method parse 9816 * @param {String} html Html string to sax parse. 9817 * @param {Object} args Optional args object that gets passed to all filter functions. 9818 * @return {tinymce.html.Node} Root node containing the tree. 9819 */ 9820 self.parse = function(html, args) { 9821 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate; 9822 var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement; 9823 var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements; 9824 var children, nonEmptyElements, rootBlockName; 9825 9826 args = args || {}; 9827 matchedNodes = {}; 9828 matchedAttributes = {}; 9829 blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); 9830 nonEmptyElements = schema.getNonEmptyElements(); 9831 children = schema.children; 9832 validate = settings.validate; 9833 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; 9834 9835 whiteSpaceElements = schema.getWhiteSpaceElements(); 9836 startWhiteSpaceRegExp = /^[ \t\r\n]+/; 9837 endWhiteSpaceRegExp = /[ \t\r\n]+$/; 9838 allWhiteSpaceRegExp = /[ \t\r\n]+/g; 9839 isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; 9840 9841 function addRootBlocks() { 9842 var node = rootNode.firstChild, next, rootBlockNode; 9843 9844 // Removes whitespace at beginning and end of block so: 9845 // <p> x </p> -> <p>x</p> 9846 function trim(rootBlockNode) { 9847 if (rootBlockNode) { 9848 node = rootBlockNode.firstChild; 9849 if (node && node.type == 3) { 9850 node.value = node.value.replace(startWhiteSpaceRegExp, ''); 9851 } 9852 9853 node = rootBlockNode.lastChild; 9854 if (node && node.type == 3) { 9855 node.value = node.value.replace(endWhiteSpaceRegExp, ''); 9856 } 9857 } 9858 } 9859 9860 // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root 9861 if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { 9862 return; 9863 } 9864 9865 while (node) { 9866 next = node.next; 9867 9868 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && 9869 !blockElements[node.name] && !node.attr('data-mce-type'))) { 9870 if (!rootBlockNode) { 9871 // Create a new root block element 9872 rootBlockNode = createNode(rootBlockName, 1); 9873 rootBlockNode.attr(settings.forced_root_block_attrs); 9874 rootNode.insert(rootBlockNode, node); 9875 rootBlockNode.append(node); 9876 } else { 9877 rootBlockNode.append(node); 9878 } 9879 } else { 9880 trim(rootBlockNode); 9881 rootBlockNode = null; 9882 } 9883 9884 node = next; 9885 } 9886 9887 trim(rootBlockNode); 9888 } 9889 9890 function createNode(name, type) { 9891 var node = new Node(name, type), list; 9892 9893 if (name in nodeFilters) { 9894 list = matchedNodes[name]; 9895 9896 if (list) { 9897 list.push(node); 9898 } else { 9899 matchedNodes[name] = [node]; 9900 } 9901 } 9902 9903 return node; 9904 } 9905 9906 function removeWhitespaceBefore(node) { 9907 var textNode, textVal, sibling; 9908 9909 for (textNode = node.prev; textNode && textNode.type === 3;) { 9910 textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); 9911 9912 if (textVal.length > 0) { 9913 textNode.value = textVal; 9914 textNode = textNode.prev; 9915 } else { 9916 sibling = textNode.prev; 9917 textNode.remove(); 9918 textNode = sibling; 9919 } 9920 } 9921 } 9922 9923 function cloneAndExcludeBlocks(input) { 9924 var name, output = {}; 9925 9926 for (name in input) { 9927 if (name !== 'li' && name != 'p') { 9928 output[name] = input[name]; 9929 } 9930 } 9931 9932 return output; 9933 } 9934 9935 parser = new SaxParser({ 9936 validate: validate, 9937 allow_script_urls: settings.allow_script_urls, 9938 allow_conditional_comments: settings.allow_conditional_comments, 9939 9940 // Exclude P and LI from DOM parsing since it's treated better by the DOM parser 9941 self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), 9942 9943 cdata: function(text) { 9944 node.append(createNode('#cdata', 4)).value = text; 9945 }, 9946 9947 text: function(text, raw) { 9948 var textNode; 9949 9950 // Trim all redundant whitespace on non white space elements 9951 if (!isInWhiteSpacePreservedElement) { 9952 text = text.replace(allWhiteSpaceRegExp, ' '); 9953 9954 if (node.lastChild && blockElements[node.lastChild.name]) { 9955 text = text.replace(startWhiteSpaceRegExp, ''); 9956 } 9957 } 9958 9959 // Do we need to create the node 9960 if (text.length !== 0) { 9961 textNode = createNode('#text', 3); 9962 textNode.raw = !!raw; 9963 node.append(textNode).value = text; 9964 } 9965 }, 9966 9967 comment: function(text) { 9968 node.append(createNode('#comment', 8)).value = text; 9969 }, 9970 9971 pi: function(name, text) { 9972 node.append(createNode(name, 7)).value = text; 9973 removeWhitespaceBefore(node); 9974 }, 9975 9976 doctype: function(text) { 9977 var newNode; 9978 9979 newNode = node.append(createNode('#doctype', 10)); 9980 newNode.value = text; 9981 removeWhitespaceBefore(node); 9982 }, 9983 9984 start: function(name, attrs, empty) { 9985 var newNode, attrFiltersLen, elementRule, attrName, parent; 9986 9987 elementRule = validate ? schema.getElementRule(name) : {}; 9988 if (elementRule) { 9989 newNode = createNode(elementRule.outputName || name, 1); 9990 newNode.attributes = attrs; 9991 newNode.shortEnded = empty; 9992 9993 node.append(newNode); 9994 9995 // Check if node is valid child of the parent node is the child is 9996 // unknown we don't collect it since it's probably a custom element 9997 parent = children[node.name]; 9998 if (parent && children[newNode.name] && !parent[newNode.name]) { 9999 invalidChildren.push(newNode); 10000 } 10001 10002 attrFiltersLen = attributeFilters.length; 10003 while (attrFiltersLen--) { 10004 attrName = attributeFilters[attrFiltersLen].name; 10005 10006 if (attrName in attrs.map) { 10007 list = matchedAttributes[attrName]; 10008 10009 if (list) { 10010 list.push(newNode); 10011 } else { 10012 matchedAttributes[attrName] = [newNode]; 10013 } 10014 } 10015 } 10016 10017 // Trim whitespace before block 10018 if (blockElements[name]) { 10019 removeWhitespaceBefore(newNode); 10020 } 10021 10022 // Change current node if the element wasn't empty i.e not <br /> or <img /> 10023 if (!empty) { 10024 node = newNode; 10025 } 10026 10027 // Check if we are inside a whitespace preserved element 10028 if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 10029 isInWhiteSpacePreservedElement = true; 10030 } 10031 } 10032 }, 10033 10034 end: function(name) { 10035 var textNode, elementRule, text, sibling, tempNode; 10036 10037 elementRule = validate ? schema.getElementRule(name) : {}; 10038 if (elementRule) { 10039 if (blockElements[name]) { 10040 if (!isInWhiteSpacePreservedElement) { 10041 // Trim whitespace of the first node in a block 10042 textNode = node.firstChild; 10043 if (textNode && textNode.type === 3) { 10044 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 10045 10046 // Any characters left after trim or should we remove it 10047 if (text.length > 0) { 10048 textNode.value = text; 10049 textNode = textNode.next; 10050 } else { 10051 sibling = textNode.next; 10052 textNode.remove(); 10053 textNode = sibling; 10054 10055 // Remove any pure whitespace siblings 10056 while (textNode && textNode.type === 3) { 10057 text = textNode.value; 10058 sibling = textNode.next; 10059 10060 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 10061 textNode.remove(); 10062 textNode = sibling; 10063 } 10064 10065 textNode = sibling; 10066 } 10067 } 10068 } 10069 10070 // Trim whitespace of the last node in a block 10071 textNode = node.lastChild; 10072 if (textNode && textNode.type === 3) { 10073 text = textNode.value.replace(endWhiteSpaceRegExp, ''); 10074 10075 // Any characters left after trim or should we remove it 10076 if (text.length > 0) { 10077 textNode.value = text; 10078 textNode = textNode.prev; 10079 } else { 10080 sibling = textNode.prev; 10081 textNode.remove(); 10082 textNode = sibling; 10083 10084 // Remove any pure whitespace siblings 10085 while (textNode && textNode.type === 3) { 10086 text = textNode.value; 10087 sibling = textNode.prev; 10088 10089 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 10090 textNode.remove(); 10091 textNode = sibling; 10092 } 10093 10094 textNode = sibling; 10095 } 10096 } 10097 } 10098 } 10099 10100 // Trim start white space 10101 // Removed due to: #5424 10102 /*textNode = node.prev; 10103 if (textNode && textNode.type === 3) { 10104 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 10105 10106 if (text.length > 0) 10107 textNode.value = text; 10108 else 10109 textNode.remove(); 10110 }*/ 10111 } 10112 10113 // Check if we exited a whitespace preserved element 10114 if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 10115 isInWhiteSpacePreservedElement = false; 10116 } 10117 10118 // Handle empty nodes 10119 if (elementRule.removeEmpty || elementRule.paddEmpty) { 10120 if (node.isEmpty(nonEmptyElements)) { 10121 if (elementRule.paddEmpty) { 10122 node.empty().append(new Node('#text', '3')).value = '\u00a0'; 10123 } else { 10124 // Leave nodes that have a name like <a name="name"> 10125 if (!node.attributes.map.name && !node.attributes.map.id) { 10126 tempNode = node.parent; 10127 10128 if (blockElements[node.name]) { 10129 node.empty().remove(); 10130 } else { 10131 node.unwrap(); 10132 } 10133 10134 node = tempNode; 10135 return; 10136 } 10137 } 10138 } 10139 } 10140 10141 node = node.parent; 10142 } 10143 } 10144 }, schema); 10145 10146 rootNode = node = new Node(args.context || settings.root_name, 11); 10147 10148 parser.parse(html); 10149 10150 // Fix invalid children or report invalid children in a contextual parsing 10151 if (validate && invalidChildren.length) { 10152 if (!args.context) { 10153 fixInvalidChildren(invalidChildren); 10154 } else { 10155 args.invalid = true; 10156 } 10157 } 10158 10159 // Wrap nodes in the root into block elements if the root is body 10160 if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) { 10161 addRootBlocks(); 10162 } 10163 10164 // Run filters only when the contents is valid 10165 if (!args.invalid) { 10166 // Run node filters 10167 for (name in matchedNodes) { 10168 list = nodeFilters[name]; 10169 nodes = matchedNodes[name]; 10170 10171 // Remove already removed children 10172 fi = nodes.length; 10173 while (fi--) { 10174 if (!nodes[fi].parent) { 10175 nodes.splice(fi, 1); 10176 } 10177 } 10178 10179 for (i = 0, l = list.length; i < l; i++) { 10180 list[i](nodes, name, args); 10181 } 10182 } 10183 10184 // Run attribute filters 10185 for (i = 0, l = attributeFilters.length; i < l; i++) { 10186 list = attributeFilters[i]; 10187 10188 if (list.name in matchedAttributes) { 10189 nodes = matchedAttributes[list.name]; 10190 10191 // Remove already removed children 10192 fi = nodes.length; 10193 while (fi--) { 10194 if (!nodes[fi].parent) { 10195 nodes.splice(fi, 1); 10196 } 10197 } 10198 10199 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) { 10200 list.callbacks[fi](nodes, list.name, args); 10201 } 10202 } 10203 } 10204 } 10205 10206 return rootNode; 10207 }; 10208 10209 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 10210 // make it possible to place the caret inside empty blocks. This logic tries to remove 10211 // these elements and keep br elements that where intended to be there intact 10212 if (settings.remove_trailing_brs) { 10213 self.addNodeFilter('br', function(nodes) { 10214 var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements()); 10215 var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; 10216 var elementRule, textNode; 10217 10218 // Remove brs from body element as well 10219 blockElements.body = 1; 10220 10221 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 10222 for (i = 0; i < l; i++) { 10223 node = nodes[i]; 10224 parent = node.parent; 10225 10226 if (blockElements[node.parent.name] && node === parent.lastChild) { 10227 // Loop all nodes to the left of the current node and check for other BR elements 10228 // excluding bookmarks since they are invisible 10229 prev = node.prev; 10230 while (prev) { 10231 prevName = prev.name; 10232 10233 // Ignore bookmarks 10234 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { 10235 // Found a non BR element 10236 if (prevName !== "br") { 10237 break; 10238 } 10239 10240 // Found another br it's a <br><br> structure then don't remove anything 10241 if (prevName === 'br') { 10242 node = null; 10243 break; 10244 } 10245 } 10246 10247 prev = prev.prev; 10248 } 10249 10250 if (node) { 10251 node.remove(); 10252 10253 // Is the parent to be considered empty after we removed the BR 10254 if (parent.isEmpty(nonEmptyElements)) { 10255 elementRule = schema.getElementRule(parent.name); 10256 10257 // Remove or padd the element depending on schema rule 10258 if (elementRule) { 10259 if (elementRule.removeEmpty) { 10260 parent.remove(); 10261 } else if (elementRule.paddEmpty) { 10262 parent.empty().append(new Node('#text', 3)).value = '\u00a0'; 10263 } 10264 } 10265 } 10266 } 10267 } else { 10268 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> 10269 // so they become <p><b><i> </i></b></p> 10270 lastParent = node; 10271 while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { 10272 lastParent = parent; 10273 10274 if (blockElements[parent.name]) { 10275 break; 10276 } 10277 10278 parent = parent.parent; 10279 } 10280 10281 if (lastParent === parent) { 10282 textNode = new Node('#text', 3); 10283 textNode.value = '\u00a0'; 10284 node.replace(textNode); 10285 } 10286 } 10287 } 10288 }); 10289 } 10290 10291 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 10292 if (!settings.allow_html_in_named_anchor) { 10293 self.addAttributeFilter('id,name', function(nodes) { 10294 var i = nodes.length, sibling, prevSibling, parent, node; 10295 10296 while (i--) { 10297 node = nodes[i]; 10298 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 10299 parent = node.parent; 10300 10301 // Move children after current node 10302 sibling = node.lastChild; 10303 do { 10304 prevSibling = sibling.prev; 10305 parent.insert(sibling, node); 10306 sibling = prevSibling; 10307 } while (sibling); 10308 } 10309 } 10310 }); 10311 } 10312 10313 if (settings.validate && schema.getValidClasses()) { 10314 self.addAttributeFilter('class', function(nodes) { 10315 var i = nodes.length, node, classList, ci, className, classValue; 10316 var validClasses = schema.getValidClasses(), validClassesMap, valid; 10317 10318 while (i--) { 10319 node = nodes[i]; 10320 classList = node.attr('class').split(' '); 10321 classValue = ''; 10322 10323 for (ci = 0; ci < classList.length; ci++) { 10324 className = classList[ci]; 10325 valid = false; 10326 10327 validClassesMap = validClasses['*']; 10328 if (validClassesMap && validClassesMap[className]) { 10329 valid = true; 10330 } 10331 10332 validClassesMap = validClasses[node.name]; 10333 if (!valid && validClassesMap && !validClassesMap[className]) { 10334 valid = true; 10335 } 10336 10337 if (valid) { 10338 if (classValue) { 10339 classValue += ' '; 10340 } 10341 10342 classValue += className; 10343 } 10344 } 10345 10346 if (!classValue.length) { 10347 classValue = null; 10348 } 10349 10350 node.attr('class', classValue); 10351 } 10352 }); 10353 } 10354 }; 10355 }); 10356 10357 // Included from: js/tinymce/classes/html/Writer.js 10358 10359 /** 10360 * Writer.js 10361 * 10362 * Copyright, Moxiecode Systems AB 10363 * Released under LGPL License. 10364 * 10365 * License: http://www.tinymce.com/license 10366 * Contributing: http://www.tinymce.com/contributing 10367 */ 10368 10369 /** 10370 * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. 10371 * 10372 * @class tinymce.html.Writer 10373 * @example 10374 * var writer = new tinymce.html.Writer({indent: true}); 10375 * var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>'); 10376 * console.log(writer.getContent()); 10377 * 10378 * @class tinymce.html.Writer 10379 * @version 3.4 10380 */ 10381 define("tinymce/html/Writer", [ 10382 "tinymce/html/Entities", 10383 "tinymce/util/Tools" 10384 ], function(Entities, Tools) { 10385 var makeMap = Tools.makeMap; 10386 10387 /** 10388 * Constructs a new Writer instance. 10389 * 10390 * @constructor 10391 * @method Writer 10392 * @param {Object} settings Name/value settings object. 10393 */ 10394 return function(settings) { 10395 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; 10396 10397 settings = settings || {}; 10398 indent = settings.indent; 10399 indentBefore = makeMap(settings.indent_before || ''); 10400 indentAfter = makeMap(settings.indent_after || ''); 10401 encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 10402 htmlOutput = settings.element_format == "html"; 10403 10404 return { 10405 /** 10406 * Writes the a start element such as <p id="a">. 10407 * 10408 * @method start 10409 * @param {String} name Name of the element. 10410 * @param {Array} attrs Optional attribute array or undefined if it hasn't any. 10411 * @param {Boolean} empty Optional empty state if the tag should end like <br />. 10412 */ 10413 start: function(name, attrs, empty) { 10414 var i, l, attr, value; 10415 10416 if (indent && indentBefore[name] && html.length > 0) { 10417 value = html[html.length - 1]; 10418 10419 if (value.length > 0 && value !== '\n') { 10420 html.push('\n'); 10421 } 10422 } 10423 10424 html.push('<', name); 10425 10426 if (attrs) { 10427 for (i = 0, l = attrs.length; i < l; i++) { 10428 attr = attrs[i]; 10429 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 10430 } 10431 } 10432 10433 if (!empty || htmlOutput) { 10434 html[html.length] = '>'; 10435 } else { 10436 html[html.length] = ' />'; 10437 } 10438 10439 if (empty && indent && indentAfter[name] && html.length > 0) { 10440 value = html[html.length - 1]; 10441 10442 if (value.length > 0 && value !== '\n') { 10443 html.push('\n'); 10444 } 10445 } 10446 }, 10447 10448 /** 10449 * Writes the a end element such as </p>. 10450 * 10451 * @method end 10452 * @param {String} name Name of the element. 10453 */ 10454 end: function(name) { 10455 var value; 10456 10457 /*if (indent && indentBefore[name] && html.length > 0) { 10458 value = html[html.length - 1]; 10459 10460 if (value.length > 0 && value !== '\n') 10461 html.push('\n'); 10462 }*/ 10463 10464 html.push('</', name, '>'); 10465 10466 if (indent && indentAfter[name] && html.length > 0) { 10467 value = html[html.length - 1]; 10468 10469 if (value.length > 0 && value !== '\n') { 10470 html.push('\n'); 10471 } 10472 } 10473 }, 10474 10475 /** 10476 * Writes a text node. 10477 * 10478 * @method text 10479 * @param {String} text String to write out. 10480 * @param {Boolean} raw Optional raw state if true the contents wont get encoded. 10481 */ 10482 text: function(text, raw) { 10483 if (text.length > 0) { 10484 html[html.length] = raw ? text : encode(text); 10485 } 10486 }, 10487 10488 /** 10489 * Writes a cdata node such as <![CDATA[data]]>. 10490 * 10491 * @method cdata 10492 * @param {String} text String to write out inside the cdata. 10493 */ 10494 cdata: function(text) { 10495 html.push('<![CDATA[', text, ']]>'); 10496 }, 10497 10498 /** 10499 * Writes a comment node such as <!-- Comment -->. 10500 * 10501 * @method cdata 10502 * @param {String} text String to write out inside the comment. 10503 */ 10504 comment: function(text) { 10505 html.push('<!--', text, '-->'); 10506 }, 10507 10508 /** 10509 * Writes a PI node such as <?xml attr="value" ?>. 10510 * 10511 * @method pi 10512 * @param {String} name Name of the pi. 10513 * @param {String} text String to write out inside the pi. 10514 */ 10515 pi: function(name, text) { 10516 if (text) { 10517 html.push('<?', name, ' ', text, '?>'); 10518 } else { 10519 html.push('<?', name, '?>'); 10520 } 10521 10522 if (indent) { 10523 html.push('\n'); 10524 } 10525 }, 10526 10527 /** 10528 * Writes a doctype node such as <!DOCTYPE data>. 10529 * 10530 * @method doctype 10531 * @param {String} text String to write out inside the doctype. 10532 */ 10533 doctype: function(text) { 10534 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 10535 }, 10536 10537 /** 10538 * Resets the internal buffer if one wants to reuse the writer. 10539 * 10540 * @method reset 10541 */ 10542 reset: function() { 10543 html.length = 0; 10544 }, 10545 10546 /** 10547 * Returns the contents that got serialized. 10548 * 10549 * @method getContent 10550 * @return {String} HTML contents that got written down. 10551 */ 10552 getContent: function() { 10553 return html.join('').replace(/\n$/, ''); 10554 } 10555 }; 10556 }; 10557 }); 10558 10559 // Included from: js/tinymce/classes/html/Serializer.js 10560 10561 /** 10562 * Serializer.js 10563 * 10564 * Copyright, Moxiecode Systems AB 10565 * Released under LGPL License. 10566 * 10567 * License: http://www.tinymce.com/license 10568 * Contributing: http://www.tinymce.com/contributing 10569 */ 10570 10571 /** 10572 * This class is used to serialize down the DOM tree into a string using a Writer instance. 10573 * 10574 * 10575 * @example 10576 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>')); 10577 * @class tinymce.html.Serializer 10578 * @version 3.4 10579 */ 10580 define("tinymce/html/Serializer", [ 10581 "tinymce/html/Writer", 10582 "tinymce/html/Schema" 10583 ], function(Writer, Schema) { 10584 /** 10585 * Constructs a new Serializer instance. 10586 * 10587 * @constructor 10588 * @method Serializer 10589 * @param {Object} settings Name/value settings object. 10590 * @param {tinymce.html.Schema} schema Schema instance to use. 10591 */ 10592 return function(settings, schema) { 10593 var self = this, writer = new Writer(settings); 10594 10595 settings = settings || {}; 10596 settings.validate = "validate" in settings ? settings.validate : true; 10597 10598 self.schema = schema = schema || new Schema(); 10599 self.writer = writer; 10600 10601 /** 10602 * Serializes the specified node into a string. 10603 * 10604 * @example 10605 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>')); 10606 * @method serialize 10607 * @param {tinymce.html.Node} node Node instance to serialize. 10608 * @return {String} String with HTML based on DOM tree. 10609 */ 10610 self.serialize = function(node) { 10611 var handlers, validate; 10612 10613 validate = settings.validate; 10614 10615 handlers = { 10616 // #text 10617 3: function(node) { 10618 writer.text(node.value, node.raw); 10619 }, 10620 10621 // #comment 10622 8: function(node) { 10623 writer.comment(node.value); 10624 }, 10625 10626 // Processing instruction 10627 7: function(node) { 10628 writer.pi(node.name, node.value); 10629 }, 10630 10631 // Doctype 10632 10: function(node) { 10633 writer.doctype(node.value); 10634 }, 10635 10636 // CDATA 10637 4: function(node) { 10638 writer.cdata(node.value); 10639 }, 10640 10641 // Document fragment 10642 11: function(node) { 10643 if ((node = node.firstChild)) { 10644 do { 10645 walk(node); 10646 } while ((node = node.next)); 10647 } 10648 } 10649 }; 10650 10651 writer.reset(); 10652 10653 function walk(node) { 10654 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; 10655 10656 if (!handler) { 10657 name = node.name; 10658 isEmpty = node.shortEnded; 10659 attrs = node.attributes; 10660 10661 // Sort attributes 10662 if (validate && attrs && attrs.length > 1) { 10663 sortedAttrs = []; 10664 sortedAttrs.map = {}; 10665 10666 elementRule = schema.getElementRule(node.name); 10667 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 10668 attrName = elementRule.attributesOrder[i]; 10669 10670 if (attrName in attrs.map) { 10671 attrValue = attrs.map[attrName]; 10672 sortedAttrs.map[attrName] = attrValue; 10673 sortedAttrs.push({name: attrName, value: attrValue}); 10674 } 10675 } 10676 10677 for (i = 0, l = attrs.length; i < l; i++) { 10678 attrName = attrs[i].name; 10679 10680 if (!(attrName in sortedAttrs.map)) { 10681 attrValue = attrs.map[attrName]; 10682 sortedAttrs.map[attrName] = attrValue; 10683 sortedAttrs.push({name: attrName, value: attrValue}); 10684 } 10685 } 10686 10687 attrs = sortedAttrs; 10688 } 10689 10690 writer.start(node.name, attrs, isEmpty); 10691 10692 if (!isEmpty) { 10693 if ((node = node.firstChild)) { 10694 do { 10695 walk(node); 10696 } while ((node = node.next)); 10697 } 10698 10699 writer.end(name); 10700 } 10701 } else { 10702 handler(node); 10703 } 10704 } 10705 10706 // Serialize element and treat all non elements as fragments 10707 if (node.type == 1 && !settings.inner) { 10708 walk(node); 10709 } else { 10710 handlers[11](node); 10711 } 10712 10713 return writer.getContent(); 10714 }; 10715 }; 10716 }); 10717 10718 // Included from: js/tinymce/classes/dom/Serializer.js 10719 10720 /** 10721 * Serializer.js 10722 * 10723 * Copyright, Moxiecode Systems AB 10724 * Released under LGPL License. 10725 * 10726 * License: http://www.tinymce.com/license 10727 * Contributing: http://www.tinymce.com/contributing 10728 */ 10729 10730 /** 10731 * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for 10732 * more details and examples on how to use this class. 10733 * 10734 * @class tinymce.dom.Serializer 10735 */ 10736 define("tinymce/dom/Serializer", [ 10737 "tinymce/dom/DOMUtils", 10738 "tinymce/html/DomParser", 10739 "tinymce/html/Entities", 10740 "tinymce/html/Serializer", 10741 "tinymce/html/Node", 10742 "tinymce/html/Schema", 10743 "tinymce/Env", 10744 "tinymce/util/Tools" 10745 ], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) { 10746 var each = Tools.each, trim = Tools.trim; 10747 var DOM = DOMUtils.DOM; 10748 10749 /** 10750 * Constructs a new DOM serializer class. 10751 * 10752 * @constructor 10753 * @method Serializer 10754 * @param {Object} settings Serializer settings object. 10755 * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. 10756 */ 10757 return function(settings, editor) { 10758 var dom, schema, htmlParser; 10759 10760 if (editor) { 10761 dom = editor.dom; 10762 schema = editor.schema; 10763 } 10764 10765 // Default DOM and Schema if they are undefined 10766 dom = dom || DOM; 10767 schema = schema || new Schema(settings); 10768 settings.entity_encoding = settings.entity_encoding || 'named'; 10769 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; 10770 10771 htmlParser = new DomParser(settings, schema); 10772 10773 // Convert tabindex back to elements when serializing contents 10774 htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) { 10775 var i = nodes.length, node; 10776 10777 while (i--) { 10778 node = nodes[i]; 10779 node.attr('tabindex', node.attributes.map['data-mce-tabindex']); 10780 node.attr(name, null); 10781 } 10782 }); 10783 10784 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 10785 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { 10786 var i = nodes.length, node, value, internalName = 'data-mce-' + name; 10787 var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; 10788 10789 while (i--) { 10790 node = nodes[i]; 10791 10792 value = node.attributes.map[internalName]; 10793 if (value !== undef) { 10794 // Set external name to internal value and remove internal 10795 node.attr(name, value.length > 0 ? value : null); 10796 node.attr(internalName, null); 10797 } else { 10798 // No internal attribute found then convert the value we have in the DOM 10799 value = node.attributes.map[name]; 10800 10801 if (name === "style") { 10802 value = dom.serializeStyle(dom.parseStyle(value), node.name); 10803 } else if (urlConverter) { 10804 value = urlConverter.call(urlConverterScope, value, name, node.name); 10805 } 10806 10807 node.attr(name, value.length > 0 ? value : null); 10808 } 10809 } 10810 }); 10811 10812 // Remove internal classes mceItem<..> or mceSelected 10813 htmlParser.addAttributeFilter('class', function(nodes) { 10814 var i = nodes.length, node, value; 10815 10816 while (i--) { 10817 node = nodes[i]; 10818 value = node.attr('class'); 10819 10820 if (value) { 10821 value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); 10822 node.attr('class', value.length > 0 ? value : null); 10823 } 10824 } 10825 }); 10826 10827 // Remove bookmark elements 10828 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { 10829 var i = nodes.length, node; 10830 10831 while (i--) { 10832 node = nodes[i]; 10833 10834 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { 10835 node.remove(); 10836 } 10837 } 10838 }); 10839 10840 htmlParser.addNodeFilter('noscript', function(nodes) { 10841 var i = nodes.length, node; 10842 10843 while (i--) { 10844 node = nodes[i].firstChild; 10845 10846 if (node) { 10847 node.value = Entities.decode(node.value); 10848 } 10849 } 10850 }); 10851 10852 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 10853 htmlParser.addNodeFilter('script,style', function(nodes, name) { 10854 var i = nodes.length, node, value, type; 10855 10856 function trim(value) { 10857 /*jshint maxlen:255 */ 10858 /*eslint max-len:0 */ 10859 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 10860 .replace(/^[\r\n]*|[\r\n]*$/g, '') 10861 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 10862 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 10863 } 10864 10865 while (i--) { 10866 node = nodes[i]; 10867 value = node.firstChild ? node.firstChild.value : ''; 10868 10869 if (name === "script") { 10870 // Remove mce- prefix from script elements and remove default type since the user specified 10871 // a script element without type attribute 10872 type = node.attr('type'); 10873 if (type) { 10874 node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, '')); 10875 } 10876 10877 if (value.length > 0) { 10878 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 10879 } 10880 } else { 10881 if (value.length > 0) { 10882 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; 10883 } 10884 } 10885 } 10886 }); 10887 10888 // Convert comments to cdata and handle protected comments 10889 htmlParser.addNodeFilter('#comment', function(nodes) { 10890 var i = nodes.length, node; 10891 10892 while (i--) { 10893 node = nodes[i]; 10894 10895 if (node.value.indexOf('[CDATA[') === 0) { 10896 node.name = '#cdata'; 10897 node.type = 4; 10898 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); 10899 } else if (node.value.indexOf('mce:protected ') === 0) { 10900 node.name = "#text"; 10901 node.type = 3; 10902 node.raw = true; 10903 node.value = unescape(node.value).substr(14); 10904 } 10905 } 10906 }); 10907 10908 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { 10909 var i = nodes.length, node; 10910 10911 while (i--) { 10912 node = nodes[i]; 10913 if (node.type === 7) { 10914 node.remove(); 10915 } else if (node.type === 1) { 10916 if (name === "input" && !("type" in node.attributes.map)) { 10917 node.attr('type', 'text'); 10918 } 10919 } 10920 } 10921 }); 10922 10923 // Fix list elements, TODO: Replace this later 10924 if (settings.fix_list_elements) { 10925 htmlParser.addNodeFilter('ul,ol', function(nodes) { 10926 var i = nodes.length, node, parentNode; 10927 10928 while (i--) { 10929 node = nodes[i]; 10930 parentNode = node.parent; 10931 10932 if (parentNode.name === 'ul' || parentNode.name === 'ol') { 10933 if (node.prev && node.prev.name === 'li') { 10934 node.prev.append(node); 10935 } 10936 } 10937 } 10938 }); 10939 } 10940 10941 // Remove internal data attributes 10942 htmlParser.addAttributeFilter( 10943 'data-mce-src,data-mce-href,data-mce-style,' + 10944 'data-mce-selected,data-mce-expando,' + 10945 'data-mce-type,data-mce-resize', 10946 10947 function(nodes, name) { 10948 var i = nodes.length; 10949 10950 while (i--) { 10951 nodes[i].attr(name, null); 10952 } 10953 } 10954 ); 10955 10956 // Return public methods 10957 return { 10958 /** 10959 * Schema instance that was used to when the Serializer was constructed. 10960 * 10961 * @field {tinymce.html.Schema} schema 10962 */ 10963 schema: schema, 10964 10965 /** 10966 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name 10967 * and then execute the callback ones it has finished parsing the document. 10968 * 10969 * @example 10970 * parser.addNodeFilter('p,h1', function(nodes, name) { 10971 * for (var i = 0; i < nodes.length; i++) { 10972 * console.log(nodes[i].name); 10973 * } 10974 * }); 10975 * @method addNodeFilter 10976 * @method {String} name Comma separated list of nodes to collect. 10977 * @param {function} callback Callback function to execute once it has collected nodes. 10978 */ 10979 addNodeFilter: htmlParser.addNodeFilter, 10980 10981 /** 10982 * Adds a attribute filter function to the parser used by the serializer, the parser will 10983 * collect nodes that has the specified attributes 10984 * and then execute the callback ones it has finished parsing the document. 10985 * 10986 * @example 10987 * parser.addAttributeFilter('src,href', function(nodes, name) { 10988 * for (var i = 0; i < nodes.length; i++) { 10989 * console.log(nodes[i].name); 10990 * } 10991 * }); 10992 * @method addAttributeFilter 10993 * @method {String} name Comma separated list of nodes to collect. 10994 * @param {function} callback Callback function to execute once it has collected nodes. 10995 */ 10996 addAttributeFilter: htmlParser.addAttributeFilter, 10997 10998 /** 10999 * Serializes the specified browser DOM node into a HTML string. 11000 * 11001 * @method serialize 11002 * @param {DOMNode} node DOM node to serialize. 11003 * @param {Object} args Arguments option that gets passed to event handlers. 11004 */ 11005 serialize: function(node, args) { 11006 var self = this, impl, doc, oldDoc, htmlSerializer, content; 11007 11008 // Explorer won't clone contents of script and style and the 11009 // selected index of select elements are cleared on a clone operation. 11010 if (Env.ie && dom.select('script,style,select,map').length > 0) { 11011 content = node.innerHTML; 11012 node = node.cloneNode(false); 11013 dom.setHTML(node, content); 11014 } else { 11015 node = node.cloneNode(true); 11016 } 11017 11018 // Nodes needs to be attached to something in WebKit/Opera 11019 // This fix will make DOM ranges and make Sizzle happy! 11020 impl = node.ownerDocument.implementation; 11021 if (impl.createHTMLDocument) { 11022 // Create an empty HTML document 11023 doc = impl.createHTMLDocument(""); 11024 11025 // Add the element or it's children if it's a body element to the new document 11026 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { 11027 doc.body.appendChild(doc.importNode(node, true)); 11028 }); 11029 11030 // Grab first child or body element for serialization 11031 if (node.nodeName != 'BODY') { 11032 node = doc.body.firstChild; 11033 } else { 11034 node = doc.body; 11035 } 11036 11037 // set the new document in DOMUtils so createElement etc works 11038 oldDoc = dom.doc; 11039 dom.doc = doc; 11040 } 11041 11042 args = args || {}; 11043 args.format = args.format || 'html'; 11044 11045 // Don't wrap content if we want selected html 11046 if (args.selection) { 11047 args.forced_root_block = ''; 11048 } 11049 11050 // Pre process 11051 if (!args.no_events) { 11052 args.node = node; 11053 self.onPreProcess(args); 11054 } 11055 11056 // Setup serializer 11057 htmlSerializer = new Serializer(settings, schema); 11058 11059 // Parse and serialize HTML 11060 args.content = htmlSerializer.serialize( 11061 htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) 11062 ); 11063 11064 // Replace all BOM characters for now until we can find a better solution 11065 if (!args.cleanup) { 11066 args.content = args.content.replace(/\uFEFF/g, ''); 11067 } 11068 11069 // Post process 11070 if (!args.no_events) { 11071 self.onPostProcess(args); 11072 } 11073 11074 // Restore the old document if it was changed 11075 if (oldDoc) { 11076 dom.doc = oldDoc; 11077 } 11078 11079 args.node = null; 11080 11081 return args.content; 11082 }, 11083 11084 /** 11085 * Adds valid elements rules to the serializers schema instance this enables you to specify things 11086 * like what elements should be outputted and what attributes specific elements might have. 11087 * Consult the Wiki for more details on this format. 11088 * 11089 * @method addRules 11090 * @param {String} rules Valid elements rules string to add to schema. 11091 */ 11092 addRules: function(rules) { 11093 schema.addValidElements(rules); 11094 }, 11095 11096 /** 11097 * Sets the valid elements rules to the serializers schema instance this enables you to specify things 11098 * like what elements should be outputted and what attributes specific elements might have. 11099 * Consult the Wiki for more details on this format. 11100 * 11101 * @method setRules 11102 * @param {String} rules Valid elements rules string. 11103 */ 11104 setRules: function(rules) { 11105 schema.setValidElements(rules); 11106 }, 11107 11108 onPreProcess: function(args) { 11109 if (editor) { 11110 editor.fire('PreProcess', args); 11111 } 11112 }, 11113 11114 onPostProcess: function(args) { 11115 if (editor) { 11116 editor.fire('PostProcess', args); 11117 } 11118 } 11119 }; 11120 }; 11121 }); 11122 11123 // Included from: js/tinymce/classes/dom/TridentSelection.js 11124 11125 /** 11126 * TridentSelection.js 11127 * 11128 * Copyright, Moxiecode Systems AB 11129 * Released under LGPL License. 11130 * 11131 * License: http://www.tinymce.com/license 11132 * Contributing: http://www.tinymce.com/contributing 11133 */ 11134 11135 /** 11136 * Selection class for old explorer versions. This one fakes the 11137 * native selection object available on modern browsers. 11138 * 11139 * @class tinymce.dom.TridentSelection 11140 */ 11141 define("tinymce/dom/TridentSelection", [], function() { 11142 function Selection(selection) { 11143 var self = this, dom = selection.dom, FALSE = false; 11144 11145 function getPosition(rng, start) { 11146 var checkRng, startIndex = 0, endIndex, inside, 11147 children, child, offset, index, position = -1, parent; 11148 11149 // Setup test range, collapse it and get the parent 11150 checkRng = rng.duplicate(); 11151 checkRng.collapse(start); 11152 parent = checkRng.parentElement(); 11153 11154 // Check if the selection is within the right document 11155 if (parent.ownerDocument !== selection.dom.doc) { 11156 return; 11157 } 11158 11159 // IE will report non editable elements as it's parent so look for an editable one 11160 while (parent.contentEditable === "false") { 11161 parent = parent.parentNode; 11162 } 11163 11164 // If parent doesn't have any children then return that we are inside the element 11165 if (!parent.hasChildNodes()) { 11166 return {node: parent, inside: 1}; 11167 } 11168 11169 // Setup node list and endIndex 11170 children = parent.children; 11171 endIndex = children.length - 1; 11172 11173 // Perform a binary search for the position 11174 while (startIndex <= endIndex) { 11175 index = Math.floor((startIndex + endIndex) / 2); 11176 11177 // Move selection to node and compare the ranges 11178 child = children[index]; 11179 checkRng.moveToElementText(child); 11180 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); 11181 11182 // Before/after or an exact match 11183 if (position > 0) { 11184 endIndex = index - 1; 11185 } else if (position < 0) { 11186 startIndex = index + 1; 11187 } else { 11188 return {node: child}; 11189 } 11190 } 11191 11192 // Check if child position is before or we didn't find a position 11193 if (position < 0) { 11194 // No element child was found use the parent element and the offset inside that 11195 if (!child) { 11196 checkRng.moveToElementText(parent); 11197 checkRng.collapse(true); 11198 child = parent; 11199 inside = true; 11200 } else { 11201 checkRng.collapse(false); 11202 } 11203 11204 // Walk character by character in text node until we hit the selected range endpoint, 11205 // hit the end of document or parent isn't the right one 11206 // We need to walk char by char since rng.text or rng.htmlText will trim line endings 11207 offset = 0; 11208 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 11209 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { 11210 break; 11211 } 11212 11213 offset++; 11214 } 11215 } else { 11216 // Child position is after the selection endpoint 11217 checkRng.collapse(true); 11218 11219 // Walk character by character in text node until we hit the selected range endpoint, hit 11220 // the end of document or parent isn't the right one 11221 offset = 0; 11222 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 11223 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { 11224 break; 11225 } 11226 11227 offset++; 11228 } 11229 } 11230 11231 return {node: child, position: position, offset: offset, inside: inside}; 11232 } 11233 11234 // Returns a W3C DOM compatible range object by using the IE Range API 11235 function getRange() { 11236 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark; 11237 11238 // If selection is outside the current document just return an empty range 11239 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); 11240 if (element.ownerDocument != dom.doc) { 11241 return domRange; 11242 } 11243 11244 collapsed = selection.isCollapsed(); 11245 11246 // Handle control selection 11247 if (ieRange.item) { 11248 domRange.setStart(element.parentNode, dom.nodeIndex(element)); 11249 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); 11250 11251 return domRange; 11252 } 11253 11254 function findEndPoint(start) { 11255 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; 11256 11257 container = endPoint.node; 11258 offset = endPoint.offset; 11259 11260 if (endPoint.inside && !container.hasChildNodes()) { 11261 domRange[start ? 'setStart' : 'setEnd'](container, 0); 11262 return; 11263 } 11264 11265 if (offset === undef) { 11266 domRange[start ? 'setStartBefore' : 'setEndAfter'](container); 11267 return; 11268 } 11269 11270 if (endPoint.position < 0) { 11271 sibling = endPoint.inside ? container.firstChild : container.nextSibling; 11272 11273 if (!sibling) { 11274 domRange[start ? 'setStartAfter' : 'setEndAfter'](container); 11275 return; 11276 } 11277 11278 if (!offset) { 11279 if (sibling.nodeType == 3) { 11280 domRange[start ? 'setStart' : 'setEnd'](sibling, 0); 11281 } else { 11282 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); 11283 } 11284 11285 return; 11286 } 11287 11288 // Find the text node and offset 11289 while (sibling) { 11290 if (sibling.nodeType == 3) { 11291 nodeValue = sibling.nodeValue; 11292 textNodeOffset += nodeValue.length; 11293 11294 // We are at or passed the position we where looking for 11295 if (textNodeOffset >= offset) { 11296 container = sibling; 11297 textNodeOffset -= offset; 11298 textNodeOffset = nodeValue.length - textNodeOffset; 11299 break; 11300 } 11301 } 11302 11303 sibling = sibling.nextSibling; 11304 } 11305 } else { 11306 // Find the text node and offset 11307 sibling = container.previousSibling; 11308 11309 if (!sibling) { 11310 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); 11311 } 11312 11313 // If there isn't any text to loop then use the first position 11314 if (!offset) { 11315 if (container.nodeType == 3) { 11316 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); 11317 } else { 11318 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); 11319 } 11320 11321 return; 11322 } 11323 11324 while (sibling) { 11325 if (sibling.nodeType == 3) { 11326 textNodeOffset += sibling.nodeValue.length; 11327 11328 // We are at or passed the position we where looking for 11329 if (textNodeOffset >= offset) { 11330 container = sibling; 11331 textNodeOffset -= offset; 11332 break; 11333 } 11334 } 11335 11336 sibling = sibling.previousSibling; 11337 } 11338 } 11339 11340 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); 11341 } 11342 11343 try { 11344 // Find start point 11345 findEndPoint(true); 11346 11347 // Find end point if needed 11348 if (!collapsed) { 11349 findEndPoint(); 11350 } 11351 } catch (ex) { 11352 // IE has a nasty bug where text nodes might throw "invalid argument" when you 11353 // access the nodeValue or other properties of text nodes. This seems to happend when 11354 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. 11355 if (ex.number == -2147024809) { 11356 // Get the current selection 11357 bookmark = self.getBookmark(2); 11358 11359 // Get start element 11360 tmpRange = ieRange.duplicate(); 11361 tmpRange.collapse(true); 11362 element = tmpRange.parentElement(); 11363 11364 // Get end element 11365 if (!collapsed) { 11366 tmpRange = ieRange.duplicate(); 11367 tmpRange.collapse(false); 11368 element2 = tmpRange.parentElement(); 11369 element2.innerHTML = element2.innerHTML; 11370 } 11371 11372 // Remove the broken elements 11373 element.innerHTML = element.innerHTML; 11374 11375 // Restore the selection 11376 self.moveToBookmark(bookmark); 11377 11378 // Since the range has moved we need to re-get it 11379 ieRange = selection.getRng(); 11380 11381 // Find start point 11382 findEndPoint(true); 11383 11384 // Find end point if needed 11385 if (!collapsed) { 11386 findEndPoint(); 11387 } 11388 } else { 11389 throw ex; // Throw other errors 11390 } 11391 } 11392 11393 return domRange; 11394 } 11395 11396 this.getBookmark = function(type) { 11397 var rng = selection.getRng(), bookmark = {}; 11398 11399 function getIndexes(node) { 11400 var parent, root, children, i, indexes = []; 11401 11402 parent = node.parentNode; 11403 root = dom.getRoot().parentNode; 11404 11405 while (parent != root && parent.nodeType !== 9) { 11406 children = parent.children; 11407 11408 i = children.length; 11409 while (i--) { 11410 if (node === children[i]) { 11411 indexes.push(i); 11412 break; 11413 } 11414 } 11415 11416 node = parent; 11417 parent = parent.parentNode; 11418 } 11419 11420 return indexes; 11421 } 11422 11423 function getBookmarkEndPoint(start) { 11424 var position; 11425 11426 position = getPosition(rng, start); 11427 if (position) { 11428 return { 11429 position: position.position, 11430 offset: position.offset, 11431 indexes: getIndexes(position.node), 11432 inside: position.inside 11433 }; 11434 } 11435 } 11436 11437 // Non ubstructive bookmark 11438 if (type === 2) { 11439 // Handle text selection 11440 if (!rng.item) { 11441 bookmark.start = getBookmarkEndPoint(true); 11442 11443 if (!selection.isCollapsed()) { 11444 bookmark.end = getBookmarkEndPoint(); 11445 } 11446 } else { 11447 bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))}; 11448 } 11449 } 11450 11451 return bookmark; 11452 }; 11453 11454 this.moveToBookmark = function(bookmark) { 11455 var rng, body = dom.doc.body; 11456 11457 function resolveIndexes(indexes) { 11458 var node, i, idx, children; 11459 11460 node = dom.getRoot(); 11461 for (i = indexes.length - 1; i >= 0; i--) { 11462 children = node.children; 11463 idx = indexes[i]; 11464 11465 if (idx <= children.length - 1) { 11466 node = children[idx]; 11467 } 11468 } 11469 11470 return node; 11471 } 11472 11473 function setBookmarkEndPoint(start) { 11474 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset; 11475 11476 if (endPoint) { 11477 moveLeft = endPoint.position > 0; 11478 11479 moveRng = body.createTextRange(); 11480 moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); 11481 11482 offset = endPoint.offset; 11483 if (offset !== undef) { 11484 moveRng.collapse(endPoint.inside || moveLeft); 11485 moveRng.moveStart('character', moveLeft ? -offset : offset); 11486 } else { 11487 moveRng.collapse(start); 11488 } 11489 11490 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); 11491 11492 if (start) { 11493 rng.collapse(true); 11494 } 11495 } 11496 } 11497 11498 if (bookmark.start) { 11499 if (bookmark.start.ctrl) { 11500 rng = body.createControlRange(); 11501 rng.addElement(resolveIndexes(bookmark.start.indexes)); 11502 rng.select(); 11503 } else { 11504 rng = body.createTextRange(); 11505 setBookmarkEndPoint(true); 11506 setBookmarkEndPoint(); 11507 rng.select(); 11508 } 11509 } 11510 }; 11511 11512 this.addRange = function(rng) { 11513 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, 11514 doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; 11515 11516 function setEndPoint(start) { 11517 var container, offset, marker, tmpRng, nodes; 11518 11519 marker = dom.create('a'); 11520 container = start ? startContainer : endContainer; 11521 offset = start ? startOffset : endOffset; 11522 tmpRng = ieRng.duplicate(); 11523 11524 if (container == doc || container == doc.documentElement) { 11525 container = body; 11526 offset = 0; 11527 } 11528 11529 if (container.nodeType == 3) { 11530 container.parentNode.insertBefore(marker, container); 11531 tmpRng.moveToElementText(marker); 11532 tmpRng.moveStart('character', offset); 11533 dom.remove(marker); 11534 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 11535 } else { 11536 nodes = container.childNodes; 11537 11538 if (nodes.length) { 11539 if (offset >= nodes.length) { 11540 dom.insertAfter(marker, nodes[nodes.length - 1]); 11541 } else { 11542 container.insertBefore(marker, nodes[offset]); 11543 } 11544 11545 tmpRng.moveToElementText(marker); 11546 } else if (container.canHaveHTML) { 11547 // Empty node selection for example <div>|</div> 11548 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open 11549 container.innerHTML = '<span></span>'; 11550 marker = container.firstChild; 11551 tmpRng.moveToElementText(marker); 11552 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason 11553 } 11554 11555 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 11556 dom.remove(marker); 11557 } 11558 } 11559 11560 // Setup some shorter versions 11561 startContainer = rng.startContainer; 11562 startOffset = rng.startOffset; 11563 endContainer = rng.endContainer; 11564 endOffset = rng.endOffset; 11565 ieRng = body.createTextRange(); 11566 11567 // If single element selection then try making a control selection out of it 11568 if (startContainer == endContainer && startContainer.nodeType == 1) { 11569 // Trick to place the caret inside an empty block element like <p></p> 11570 if (startOffset == endOffset && !startContainer.hasChildNodes()) { 11571 if (startContainer.canHaveHTML) { 11572 // Check if previous sibling is an empty block if it is then we need to render it 11573 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 11574 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> 11575 sibling = startContainer.previousSibling; 11576 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { 11577 sibling.innerHTML = ''; 11578 } else { 11579 sibling = null; 11580 } 11581 11582 startContainer.innerHTML = '<span></span><span></span>'; 11583 ieRng.moveToElementText(startContainer.lastChild); 11584 ieRng.select(); 11585 dom.doc.selection.clear(); 11586 startContainer.innerHTML = ''; 11587 11588 if (sibling) { 11589 sibling.innerHTML = ''; 11590 } 11591 return; 11592 } else { 11593 startOffset = dom.nodeIndex(startContainer); 11594 startContainer = startContainer.parentNode; 11595 } 11596 } 11597 11598 if (startOffset == endOffset - 1) { 11599 try { 11600 ctrlElm = startContainer.childNodes[startOffset]; 11601 ctrlRng = body.createControlRange(); 11602 ctrlRng.addElement(ctrlElm); 11603 ctrlRng.select(); 11604 11605 // Check if the range produced is on the correct element and is a control range 11606 // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 11607 nativeRng = selection.getRng(); 11608 if (nativeRng.item && ctrlElm === nativeRng.item(0)) { 11609 return; 11610 } 11611 } catch (ex) { 11612 // Ignore 11613 } 11614 } 11615 } 11616 11617 // Set start/end point of selection 11618 setEndPoint(true); 11619 setEndPoint(); 11620 11621 // Select the new range and scroll it into view 11622 ieRng.select(); 11623 }; 11624 11625 // Expose range method 11626 this.getRangeAt = getRange; 11627 } 11628 11629 return Selection; 11630 }); 11631 11632 // Included from: js/tinymce/classes/util/VK.js 11633 11634 /** 11635 * VK.js 11636 * 11637 * Copyright, Moxiecode Systems AB 11638 * Released under LGPL License. 11639 * 11640 * License: http://www.tinymce.com/license 11641 * Contributing: http://www.tinymce.com/contributing 11642 */ 11643 11644 /** 11645 * This file exposes a set of the common KeyCodes for use. Please grow it as needed. 11646 */ 11647 define("tinymce/util/VK", [ 11648 "tinymce/Env" 11649 ], function(Env) { 11650 return { 11651 BACKSPACE: 8, 11652 DELETE: 46, 11653 DOWN: 40, 11654 ENTER: 13, 11655 LEFT: 37, 11656 RIGHT: 39, 11657 SPACEBAR: 32, 11658 TAB: 9, 11659 UP: 38, 11660 11661 modifierPressed: function(e) { 11662 return e.shiftKey || e.ctrlKey || e.altKey; 11663 }, 11664 11665 metaKeyPressed: function(e) { 11666 // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states 11667 return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey); 11668 } 11669 }; 11670 }); 11671 11672 // Included from: js/tinymce/classes/dom/ControlSelection.js 11673 11674 /** 11675 * ControlSelection.js 11676 * 11677 * Copyright, Moxiecode Systems AB 11678 * Released under LGPL License. 11679 * 11680 * License: http://www.tinymce.com/license 11681 * Contributing: http://www.tinymce.com/contributing 11682 */ 11683 11684 /** 11685 * This class handles control selection of elements. Controls are elements 11686 * that can be resized and needs to be selected as a whole. It adds custom resize handles 11687 * to all browser engines that support properly disabling the built in resize logic. 11688 * 11689 * @class tinymce.dom.ControlSelection 11690 */ 11691 define("tinymce/dom/ControlSelection", [ 11692 "tinymce/util/VK", 11693 "tinymce/util/Tools", 11694 "tinymce/Env" 11695 ], function(VK, Tools, Env) { 11696 return function(selection, editor) { 11697 var dom = editor.dom, each = Tools.each; 11698 var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent; 11699 var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; 11700 var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; 11701 var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight; 11702 11703 // Details about each resize handle how to scale etc 11704 resizeHandles = { 11705 // Name: x multiplier, y multiplier, delta size x, delta size y 11706 n: [0.5, 0, 0, -1], 11707 e: [1, 0.5, 1, 0], 11708 s: [0.5, 1, 0, 1], 11709 w: [0, 0.5, -1, 0], 11710 nw: [0, 0, -1, -1], 11711 ne: [1, 0, 1, -1], 11712 se: [1, 1, 1, 1], 11713 sw: [0, 1, -1, 1] 11714 }; 11715 11716 // Add CSS for resize handles, cloned element and selected 11717 var rootClass = '.mce-content-body'; 11718 editor.contentStyles.push( 11719 rootClass + ' div.mce-resizehandle {' + 11720 'position: absolute;' + 11721 'border: 1px solid black;' + 11722 'background: #FFF;' + 11723 'width: 5px;' + 11724 'height: 5px;' + 11725 'z-index: 10000' + 11726 '}' + 11727 rootClass + ' .mce-resizehandle:hover {' + 11728 'background: #000' + 11729 '}' + 11730 rootClass + ' img[data-mce-selected], hr[data-mce-selected] {' + 11731 'outline: 1px solid black;' + 11732 'resize: none' + // Have been talks about implementing this in browsers 11733 '}' + 11734 rootClass + ' .mce-clonedresizable {' + 11735 'position: absolute;' + 11736 (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing 11737 'opacity: .5;' + 11738 'filter: alpha(opacity=50);' + 11739 'z-index: 10000' + 11740 '}' + 11741 rootClass + ' .mce-resize-helper {' + 11742 'background: #555;' + 11743 'background: rgba(0,0,0,0.75);' + 11744 'border-radius: 3px;' + 11745 'border: 1px;' + 11746 'color: white;' + 11747 'display: none;' + 11748 'font-family: sans-serif;' + 11749 'font-size: 12px;' + 11750 'white-space: nowrap;' + 11751 'line-height: 14px;' + 11752 'margin: 5px 10px;' + 11753 'padding: 5px;' + 11754 'position: absolute;' + 11755 'z-index: 10001' + 11756 '}' 11757 ); 11758 11759 function isResizable(elm) { 11760 var selector = editor.settings.object_resizing; 11761 11762 if (selector === false || Env.iOS) { 11763 return false; 11764 } 11765 11766 if (typeof selector != 'string') { 11767 selector = 'table,img,div'; 11768 } 11769 11770 if (elm.getAttribute('data-mce-resize') === 'false') { 11771 return false; 11772 } 11773 11774 return editor.dom.is(elm, selector); 11775 } 11776 11777 function resizeGhostElement(e) { 11778 var deltaX, deltaY, proportional; 11779 var resizeHelperX, resizeHelperY; 11780 11781 // Calc new width/height 11782 deltaX = e.screenX - startX; 11783 deltaY = e.screenY - startY; 11784 11785 // Calc new size 11786 width = deltaX * selectedHandle[2] + startW; 11787 height = deltaY * selectedHandle[3] + startH; 11788 11789 // Never scale down lower than 5 pixels 11790 width = width < 5 ? 5 : width; 11791 height = height < 5 ? 5 : height; 11792 11793 if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) { 11794 proportional = !VK.modifierPressed(e); 11795 } else { 11796 proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0); 11797 } 11798 11799 // Constrain proportions 11800 if (proportional) { 11801 if (abs(deltaX) > abs(deltaY)) { 11802 height = round(width * ratio); 11803 width = round(height / ratio); 11804 } else { 11805 width = round(height / ratio); 11806 height = round(width * ratio); 11807 } 11808 } 11809 11810 // Update ghost size 11811 dom.setStyles(selectedElmGhost, { 11812 width: width, 11813 height: height 11814 }); 11815 11816 // Update resize helper position 11817 resizeHelperX = selectedHandle.startPos.x + deltaX; 11818 resizeHelperY = selectedHandle.startPos.y + deltaY; 11819 resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0; 11820 resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0; 11821 11822 dom.setStyles(resizeHelper, { 11823 left: resizeHelperX, 11824 top: resizeHelperY, 11825 display: 'block' 11826 }); 11827 11828 resizeHelper.innerHTML = width + ' × ' + height; 11829 11830 // Update ghost X position if needed 11831 if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { 11832 dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); 11833 } 11834 11835 // Update ghost Y position if needed 11836 if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { 11837 dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); 11838 } 11839 11840 // Calculate how must overflow we got 11841 deltaX = rootElement.scrollWidth - startScrollWidth; 11842 deltaY = rootElement.scrollHeight - startScrollHeight; 11843 11844 // Re-position the resize helper based on the overflow 11845 if (deltaX + deltaY !== 0) { 11846 dom.setStyles(resizeHelper, { 11847 left: resizeHelperX - deltaX, 11848 top: resizeHelperY - deltaY 11849 }); 11850 } 11851 11852 if (!resizeStarted) { 11853 editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH}); 11854 resizeStarted = true; 11855 } 11856 } 11857 11858 function endGhostResize() { 11859 resizeStarted = false; 11860 11861 function setSizeProp(name, value) { 11862 if (value) { 11863 // Resize by using style or attribute 11864 if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { 11865 dom.setStyle(selectedElm, name, value); 11866 } else { 11867 dom.setAttrib(selectedElm, name, value); 11868 } 11869 } 11870 } 11871 11872 // Set width/height properties 11873 setSizeProp('width', width); 11874 setSizeProp('height', height); 11875 11876 dom.unbind(editableDoc, 'mousemove', resizeGhostElement); 11877 dom.unbind(editableDoc, 'mouseup', endGhostResize); 11878 11879 if (rootDocument != editableDoc) { 11880 dom.unbind(rootDocument, 'mousemove', resizeGhostElement); 11881 dom.unbind(rootDocument, 'mouseup', endGhostResize); 11882 } 11883 11884 // Remove ghost/helper and update resize handle positions 11885 dom.remove(selectedElmGhost); 11886 dom.remove(resizeHelper); 11887 11888 if (!isIE || selectedElm.nodeName == "TABLE") { 11889 showResizeRect(selectedElm); 11890 } 11891 11892 editor.fire('ObjectResized', {target: selectedElm, width: width, height: height}); 11893 dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style')); 11894 editor.nodeChanged(); 11895 } 11896 11897 function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { 11898 var position, targetWidth, targetHeight, e, rect; 11899 11900 unbindResizeHandleEvents(); 11901 11902 // Get position and size of target 11903 position = dom.getPos(targetElm, rootElement); 11904 selectedElmX = position.x; 11905 selectedElmY = position.y; 11906 rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption 11907 targetWidth = rect.width || (rect.right - rect.left); 11908 targetHeight = rect.height || (rect.bottom - rect.top); 11909 11910 // Reset width/height if user selects a new image/table 11911 if (selectedElm != targetElm) { 11912 detachResizeStartListener(); 11913 selectedElm = targetElm; 11914 width = height = 0; 11915 } 11916 11917 // Makes it possible to disable resizing 11918 e = editor.fire('ObjectSelected', {target: targetElm}); 11919 11920 if (isResizable(targetElm) && !e.isDefaultPrevented()) { 11921 each(resizeHandles, function(handle, name) { 11922 var handleElm, handlerContainerElm; 11923 11924 function startDrag(e) { 11925 startX = e.screenX; 11926 startY = e.screenY; 11927 startW = selectedElm.clientWidth; 11928 startH = selectedElm.clientHeight; 11929 ratio = startH / startW; 11930 selectedHandle = handle; 11931 11932 handle.startPos = { 11933 x: targetWidth * handle[0] + selectedElmX, 11934 y: targetHeight * handle[1] + selectedElmY 11935 }; 11936 11937 startScrollWidth = rootElement.scrollWidth; 11938 startScrollHeight = rootElement.scrollHeight; 11939 11940 selectedElmGhost = selectedElm.cloneNode(true); 11941 dom.addClass(selectedElmGhost, 'mce-clonedresizable'); 11942 dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all'); 11943 selectedElmGhost.contentEditable = false; // Hides IE move layer cursor 11944 selectedElmGhost.unSelectabe = true; 11945 dom.setStyles(selectedElmGhost, { 11946 left: selectedElmX, 11947 top: selectedElmY, 11948 margin: 0 11949 }); 11950 11951 selectedElmGhost.removeAttribute('data-mce-selected'); 11952 rootElement.appendChild(selectedElmGhost); 11953 11954 dom.bind(editableDoc, 'mousemove', resizeGhostElement); 11955 dom.bind(editableDoc, 'mouseup', endGhostResize); 11956 11957 if (rootDocument != editableDoc) { 11958 dom.bind(rootDocument, 'mousemove', resizeGhostElement); 11959 dom.bind(rootDocument, 'mouseup', endGhostResize); 11960 } 11961 11962 resizeHelper = dom.add(rootElement, 'div', { 11963 'class': 'mce-resize-helper', 11964 'data-mce-bogus': 'all' 11965 }, startW + ' × ' + startH); 11966 } 11967 11968 if (mouseDownHandleName) { 11969 // Drag started by IE native resizestart 11970 if (name == mouseDownHandleName) { 11971 startDrag(mouseDownEvent); 11972 } 11973 11974 return; 11975 } 11976 11977 // Get existing or render resize handle 11978 handleElm = dom.get('mceResizeHandle' + name); 11979 if (!handleElm) { 11980 handlerContainerElm = rootElement; 11981 11982 handleElm = dom.add(handlerContainerElm, 'div', { 11983 id: 'mceResizeHandle' + name, 11984 'data-mce-bogus': 'all', 11985 'class': 'mce-resizehandle', 11986 unselectable: true, 11987 style: 'cursor:' + name + '-resize; margin:0; padding:0' 11988 }); 11989 11990 // Hides IE move layer cursor 11991 // If we set it on Chrome we get this wounderful bug: #6725 11992 if (Env.ie) { 11993 handleElm.contentEditable = false; 11994 } 11995 } else { 11996 dom.show(handleElm); 11997 } 11998 11999 if (!handle.elm) { 12000 dom.bind(handleElm, 'mousedown', function(e) { 12001 e.stopImmediatePropagation(); 12002 e.preventDefault(); 12003 startDrag(e); 12004 }); 12005 12006 handle.elm = handleElm; 12007 } 12008 12009 // Position element 12010 dom.setStyles(handleElm, { 12011 left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), 12012 top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) 12013 }); 12014 }); 12015 } else { 12016 hideResizeRect(); 12017 } 12018 12019 selectedElm.setAttribute('data-mce-selected', '1'); 12020 } 12021 12022 function hideResizeRect() { 12023 var name, handleElm; 12024 12025 unbindResizeHandleEvents(); 12026 12027 if (selectedElm) { 12028 selectedElm.removeAttribute('data-mce-selected'); 12029 } 12030 12031 for (name in resizeHandles) { 12032 handleElm = dom.get('mceResizeHandle' + name); 12033 if (handleElm) { 12034 dom.unbind(handleElm); 12035 dom.remove(handleElm); 12036 } 12037 } 12038 } 12039 12040 function updateResizeRect(e) { 12041 var startElm, controlElm; 12042 12043 function isChildOrEqual(node, parent) { 12044 if (node) { 12045 do { 12046 if (node === parent) { 12047 return true; 12048 } 12049 } while ((node = node.parentNode)); 12050 } 12051 } 12052 12053 // Ignore all events while resizing 12054 if (resizeStarted) { 12055 return; 12056 } 12057 12058 // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v 12059 each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) { 12060 img.removeAttribute('data-mce-selected'); 12061 }); 12062 12063 controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); 12064 controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0]; 12065 12066 if (isChildOrEqual(controlElm, rootElement)) { 12067 disableGeckoResize(); 12068 startElm = selection.getStart(true); 12069 12070 if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) { 12071 if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) { 12072 showResizeRect(controlElm); 12073 return; 12074 } 12075 } 12076 } 12077 12078 hideResizeRect(); 12079 } 12080 12081 function attachEvent(elm, name, func) { 12082 if (elm && elm.attachEvent) { 12083 elm.attachEvent('on' + name, func); 12084 } 12085 } 12086 12087 function detachEvent(elm, name, func) { 12088 if (elm && elm.detachEvent) { 12089 elm.detachEvent('on' + name, func); 12090 } 12091 } 12092 12093 function resizeNativeStart(e) { 12094 var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; 12095 12096 pos = target.getBoundingClientRect(); 12097 relativeX = lastMouseDownEvent.clientX - pos.left; 12098 relativeY = lastMouseDownEvent.clientY - pos.top; 12099 12100 // Figure out what corner we are draging on 12101 for (name in resizeHandles) { 12102 corner = resizeHandles[name]; 12103 12104 cornerX = target.offsetWidth * corner[0]; 12105 cornerY = target.offsetHeight * corner[1]; 12106 12107 if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) { 12108 selectedHandle = corner; 12109 break; 12110 } 12111 } 12112 12113 // Remove native selection and let the magic begin 12114 resizeStarted = true; 12115 editor.fire('ObjectResizeStart', { 12116 target: selectedElm, 12117 width: selectedElm.clientWidth, 12118 height: selectedElm.clientHeight 12119 }); 12120 editor.getDoc().selection.empty(); 12121 showResizeRect(target, name, lastMouseDownEvent); 12122 } 12123 12124 function nativeControlSelect(e) { 12125 var target = e.srcElement; 12126 12127 if (target != selectedElm) { 12128 editor.fire('ObjectSelected', {target: target}); 12129 detachResizeStartListener(); 12130 12131 if (target.id.indexOf('mceResizeHandle') === 0) { 12132 e.returnValue = false; 12133 return; 12134 } 12135 12136 if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { 12137 hideResizeRect(); 12138 selectedElm = target; 12139 attachEvent(target, 'resizestart', resizeNativeStart); 12140 } 12141 } 12142 } 12143 12144 function detachResizeStartListener() { 12145 detachEvent(selectedElm, 'resizestart', resizeNativeStart); 12146 } 12147 12148 function unbindResizeHandleEvents() { 12149 for (var name in resizeHandles) { 12150 var handle = resizeHandles[name]; 12151 12152 if (handle.elm) { 12153 dom.unbind(handle.elm); 12154 delete handle.elm; 12155 } 12156 } 12157 } 12158 12159 function disableGeckoResize() { 12160 try { 12161 // Disable object resizing on Gecko 12162 editor.getDoc().execCommand('enableObjectResizing', false, false); 12163 } catch (ex) { 12164 // Ignore 12165 } 12166 } 12167 12168 function controlSelect(elm) { 12169 var ctrlRng; 12170 12171 if (!isIE) { 12172 return; 12173 } 12174 12175 ctrlRng = editableDoc.body.createControlRange(); 12176 12177 try { 12178 ctrlRng.addElement(elm); 12179 ctrlRng.select(); 12180 return true; 12181 } catch (ex) { 12182 // Ignore since the element can't be control selected for example a P tag 12183 } 12184 } 12185 12186 editor.on('init', function() { 12187 if (isIE) { 12188 // Hide the resize rect on resize and reselect the image 12189 editor.on('ObjectResized', function(e) { 12190 if (e.target.nodeName != 'TABLE') { 12191 hideResizeRect(); 12192 controlSelect(e.target); 12193 } 12194 }); 12195 12196 attachEvent(rootElement, 'controlselect', nativeControlSelect); 12197 12198 editor.on('mousedown', function(e) { 12199 lastMouseDownEvent = e; 12200 }); 12201 } else { 12202 disableGeckoResize(); 12203 12204 if (Env.ie >= 11) { 12205 // TODO: Drag/drop doesn't work 12206 editor.on('mouseup', function(e) { 12207 var nodeName = e.target.nodeName; 12208 12209 if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName)) { 12210 editor.selection.select(e.target, nodeName == 'TABLE'); 12211 editor.nodeChanged(); 12212 } 12213 }); 12214 12215 editor.dom.bind(rootElement, 'mscontrolselect', function(e) { 12216 if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { 12217 e.preventDefault(); 12218 12219 // This moves the selection from being a control selection to a text like selection like in WebKit #6753 12220 // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. 12221 if (e.target.tagName == 'IMG') { 12222 window.setTimeout(function() { 12223 editor.selection.select(e.target); 12224 }, 0); 12225 } 12226 } 12227 }); 12228 } 12229 } 12230 12231 editor.on('nodechange ResizeEditor', updateResizeRect); 12232 12233 // Update resize rect while typing in a table 12234 editor.on('keydown keyup', function(e) { 12235 if (selectedElm && selectedElm.nodeName == "TABLE") { 12236 updateResizeRect(e); 12237 } 12238 }); 12239 12240 editor.on('hide', hideResizeRect); 12241 12242 // Hide rect on focusout since it would float on top of windows otherwise 12243 //editor.on('focusout', hideResizeRect); 12244 }); 12245 12246 editor.on('remove', unbindResizeHandleEvents); 12247 12248 function destroy() { 12249 selectedElm = selectedElmGhost = null; 12250 12251 if (isIE) { 12252 detachResizeStartListener(); 12253 detachEvent(rootElement, 'controlselect', nativeControlSelect); 12254 } 12255 } 12256 12257 return { 12258 isResizable: isResizable, 12259 showResizeRect: showResizeRect, 12260 hideResizeRect: hideResizeRect, 12261 updateResizeRect: updateResizeRect, 12262 controlSelect: controlSelect, 12263 destroy: destroy 12264 }; 12265 }; 12266 }); 12267 12268 // Included from: js/tinymce/classes/dom/BookmarkManager.js 12269 12270 /** 12271 * BookmarkManager.js 12272 * 12273 * Copyright, Moxiecode Systems AB 12274 * Released under LGPL License. 12275 * 12276 * License: http://www.tinymce.com/license 12277 * Contributing: http://www.tinymce.com/contributing 12278 */ 12279 12280 /** 12281 * This class handles selection bookmarks. 12282 * 12283 * @class tinymce.dom.BookmarkManager 12284 */ 12285 define("tinymce/dom/BookmarkManager", [ 12286 "tinymce/Env", 12287 "tinymce/util/Tools" 12288 ], function(Env, Tools) { 12289 /** 12290 * Constructs a new BookmarkManager instance for a specific selection instance. 12291 * 12292 * @constructor 12293 * @method BookmarkManager 12294 * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for. 12295 */ 12296 function BookmarkManager(selection) { 12297 var dom = selection.dom; 12298 12299 /** 12300 * Returns a bookmark location for the current selection. This bookmark object 12301 * can then be used to restore the selection after some content modification to the document. 12302 * 12303 * @method getBookmark 12304 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. 12305 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. 12306 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. 12307 * @example 12308 * // Stores a bookmark of the current selection 12309 * var bm = tinymce.activeEditor.selection.getBookmark(); 12310 * 12311 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 12312 * 12313 * // Restore the selection bookmark 12314 * tinymce.activeEditor.selection.moveToBookmark(bm); 12315 */ 12316 this.getBookmark = function(type, normalized) { 12317 var rng, rng2, id, collapsed, name, element, chr = '', styles; 12318 12319 function findIndex(name, element) { 12320 var index = 0; 12321 12322 Tools.each(dom.select(name), function(node, i) { 12323 if (node == element) { 12324 index = i; 12325 } 12326 }); 12327 12328 return index; 12329 } 12330 12331 function normalizeTableCellSelection(rng) { 12332 function moveEndPoint(start) { 12333 var container, offset, childNodes, prefix = start ? 'start' : 'end'; 12334 12335 container = rng[prefix + 'Container']; 12336 offset = rng[prefix + 'Offset']; 12337 12338 if (container.nodeType == 1 && container.nodeName == "TR") { 12339 childNodes = container.childNodes; 12340 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 12341 if (container) { 12342 offset = start ? 0 : container.childNodes.length; 12343 rng['set' + (start ? 'Start' : 'End')](container, offset); 12344 } 12345 } 12346 } 12347 12348 moveEndPoint(true); 12349 moveEndPoint(); 12350 12351 return rng; 12352 } 12353 12354 function getLocation() { 12355 var rng = selection.getRng(true), root = dom.getRoot(), bookmark = {}; 12356 12357 function getPoint(rng, start) { 12358 var container = rng[start ? 'startContainer' : 'endContainer'], 12359 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; 12360 12361 if (container.nodeType == 3) { 12362 if (normalized) { 12363 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) { 12364 offset += node.nodeValue.length; 12365 } 12366 } 12367 12368 point.push(offset); 12369 } else { 12370 childNodes = container.childNodes; 12371 12372 if (offset >= childNodes.length && childNodes.length) { 12373 after = 1; 12374 offset = Math.max(0, childNodes.length - 1); 12375 } 12376 12377 point.push(dom.nodeIndex(childNodes[offset], normalized) + after); 12378 } 12379 12380 for (; container && container != root; container = container.parentNode) { 12381 point.push(dom.nodeIndex(container, normalized)); 12382 } 12383 12384 return point; 12385 } 12386 12387 bookmark.start = getPoint(rng, true); 12388 12389 if (!selection.isCollapsed()) { 12390 bookmark.end = getPoint(rng); 12391 } 12392 12393 return bookmark; 12394 } 12395 12396 if (type == 2) { 12397 element = selection.getNode(); 12398 name = element ? element.nodeName : null; 12399 12400 if (name == 'IMG') { 12401 return {name: name, index: findIndex(name, element)}; 12402 } 12403 12404 if (selection.tridentSel) { 12405 return selection.tridentSel.getBookmark(type); 12406 } 12407 12408 return getLocation(); 12409 } 12410 12411 // Handle simple range 12412 if (type) { 12413 return {rng: selection.getRng()}; 12414 } 12415 12416 rng = selection.getRng(); 12417 id = dom.uniqueId(); 12418 collapsed = selection.isCollapsed(); 12419 styles = 'overflow:hidden;line-height:0px'; 12420 12421 // Explorer method 12422 if (rng.duplicate || rng.item) { 12423 // Text selection 12424 if (!rng.item) { 12425 rng2 = rng.duplicate(); 12426 12427 try { 12428 // Insert start marker 12429 rng.collapse(); 12430 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); 12431 12432 // Insert end marker 12433 if (!collapsed) { 12434 rng2.collapse(false); 12435 12436 // Detect the empty space after block elements in IE and move the 12437 // end back one character <p></p>] becomes <p>]</p> 12438 rng.moveToElementText(rng2.parentElement()); 12439 if (rng.compareEndPoints('StartToEnd', rng2) === 0) { 12440 rng2.move('character', -1); 12441 } 12442 12443 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); 12444 } 12445 } catch (ex) { 12446 // IE might throw unspecified error so lets ignore it 12447 return null; 12448 } 12449 } else { 12450 // Control selection 12451 element = rng.item(0); 12452 name = element.nodeName; 12453 12454 return {name: name, index: findIndex(name, element)}; 12455 } 12456 } else { 12457 element = selection.getNode(); 12458 name = element.nodeName; 12459 if (name == 'IMG') { 12460 return {name: name, index: findIndex(name, element)}; 12461 } 12462 12463 // W3C method 12464 rng2 = normalizeTableCellSelection(rng.cloneRange()); 12465 12466 // Insert end marker 12467 if (!collapsed) { 12468 rng2.collapse(false); 12469 rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr)); 12470 } 12471 12472 rng = normalizeTableCellSelection(rng); 12473 rng.collapse(true); 12474 rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr)); 12475 } 12476 12477 selection.moveToBookmark({id: id, keep: 1}); 12478 12479 return {id: id}; 12480 }; 12481 12482 /** 12483 * Restores the selection to the specified bookmark. 12484 * 12485 * @method moveToBookmark 12486 * @param {Object} bookmark Bookmark to restore selection from. 12487 * @return {Boolean} true/false if it was successful or not. 12488 * @example 12489 * // Stores a bookmark of the current selection 12490 * var bm = tinymce.activeEditor.selection.getBookmark(); 12491 * 12492 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 12493 * 12494 * // Restore the selection bookmark 12495 * tinymce.activeEditor.selection.moveToBookmark(bm); 12496 */ 12497 this.moveToBookmark = function(bookmark) { 12498 var rng, root, startContainer, endContainer, startOffset, endOffset; 12499 12500 function setEndPoint(start) { 12501 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; 12502 12503 if (point) { 12504 offset = point[0]; 12505 12506 // Find container node 12507 for (node = root, i = point.length - 1; i >= 1; i--) { 12508 children = node.childNodes; 12509 12510 if (point[i] > children.length - 1) { 12511 return; 12512 } 12513 12514 node = children[point[i]]; 12515 } 12516 12517 // Move text offset to best suitable location 12518 if (node.nodeType === 3) { 12519 offset = Math.min(point[0], node.nodeValue.length); 12520 } 12521 12522 // Move element offset to best suitable location 12523 if (node.nodeType === 1) { 12524 offset = Math.min(point[0], node.childNodes.length); 12525 } 12526 12527 // Set offset within container node 12528 if (start) { 12529 rng.setStart(node, offset); 12530 } else { 12531 rng.setEnd(node, offset); 12532 } 12533 } 12534 12535 return true; 12536 } 12537 12538 function restoreEndPoint(suffix) { 12539 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; 12540 12541 if (marker) { 12542 node = marker.parentNode; 12543 12544 if (suffix == 'start') { 12545 if (!keep) { 12546 idx = dom.nodeIndex(marker); 12547 } else { 12548 node = marker.firstChild; 12549 idx = 1; 12550 } 12551 12552 startContainer = endContainer = node; 12553 startOffset = endOffset = idx; 12554 } else { 12555 if (!keep) { 12556 idx = dom.nodeIndex(marker); 12557 } else { 12558 node = marker.firstChild; 12559 idx = 1; 12560 } 12561 12562 endContainer = node; 12563 endOffset = idx; 12564 } 12565 12566 if (!keep) { 12567 prev = marker.previousSibling; 12568 next = marker.nextSibling; 12569 12570 // Remove all marker text nodes 12571 Tools.each(Tools.grep(marker.childNodes), function(node) { 12572 if (node.nodeType == 3) { 12573 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); 12574 } 12575 }); 12576 12577 // Remove marker but keep children if for example contents where inserted into the marker 12578 // Also remove duplicated instances of the marker for example by a 12579 // split operation or by WebKit auto split on paste feature 12580 while ((marker = dom.get(bookmark.id + '_' + suffix))) { 12581 dom.remove(marker, 1); 12582 } 12583 12584 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 12585 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market 12586 // isn't worth the effort. Sorry, Opera but it's just a fact 12587 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { 12588 idx = prev.nodeValue.length; 12589 prev.appendData(next.nodeValue); 12590 dom.remove(next); 12591 12592 if (suffix == 'start') { 12593 startContainer = endContainer = prev; 12594 startOffset = endOffset = idx; 12595 } else { 12596 endContainer = prev; 12597 endOffset = idx; 12598 } 12599 } 12600 } 12601 } 12602 } 12603 12604 function addBogus(node) { 12605 // Adds a bogus BR element for empty block elements 12606 if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { 12607 node.innerHTML = '<br data-mce-bogus="1" />'; 12608 } 12609 12610 return node; 12611 } 12612 12613 if (bookmark) { 12614 if (bookmark.start) { 12615 rng = dom.createRng(); 12616 root = dom.getRoot(); 12617 12618 if (selection.tridentSel) { 12619 return selection.tridentSel.moveToBookmark(bookmark); 12620 } 12621 12622 if (setEndPoint(true) && setEndPoint()) { 12623 selection.setRng(rng); 12624 } 12625 } else if (bookmark.id) { 12626 // Restore start/end points 12627 restoreEndPoint('start'); 12628 restoreEndPoint('end'); 12629 12630 if (startContainer) { 12631 rng = dom.createRng(); 12632 rng.setStart(addBogus(startContainer), startOffset); 12633 rng.setEnd(addBogus(endContainer), endOffset); 12634 selection.setRng(rng); 12635 } 12636 } else if (bookmark.name) { 12637 selection.select(dom.select(bookmark.name)[bookmark.index]); 12638 } else if (bookmark.rng) { 12639 selection.setRng(bookmark.rng); 12640 } 12641 } 12642 }; 12643 } 12644 12645 /** 12646 * Returns true/false if the specified node is a bookmark node or not. 12647 * 12648 * @static 12649 * @method isBookmarkNode 12650 * @param {DOMNode} node DOM Node to check if it's a bookmark node or not. 12651 * @return {Boolean} true/false if the node is a bookmark node or not. 12652 */ 12653 BookmarkManager.isBookmarkNode = function(node) { 12654 return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark'; 12655 }; 12656 12657 return BookmarkManager; 12658 }); 12659 12660 // Included from: js/tinymce/classes/dom/Selection.js 12661 12662 /** 12663 * Selection.js 12664 * 12665 * Copyright, Moxiecode Systems AB 12666 * Released under LGPL License. 12667 * 12668 * License: http://www.tinymce.com/license 12669 * Contributing: http://www.tinymce.com/contributing 12670 */ 12671 12672 /** 12673 * This class handles text and control selection it's an crossbrowser utility class. 12674 * Consult the TinyMCE Wiki API for more details and examples on how to use this class. 12675 * 12676 * @class tinymce.dom.Selection 12677 * @example 12678 * // Getting the currently selected node for the active editor 12679 * alert(tinymce.activeEditor.selection.getNode().nodeName); 12680 */ 12681 define("tinymce/dom/Selection", [ 12682 "tinymce/dom/TreeWalker", 12683 "tinymce/dom/TridentSelection", 12684 "tinymce/dom/ControlSelection", 12685 "tinymce/dom/RangeUtils", 12686 "tinymce/dom/BookmarkManager", 12687 "tinymce/Env", 12688 "tinymce/util/Tools" 12689 ], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, BookmarkManager, Env, Tools) { 12690 var each = Tools.each, trim = Tools.trim; 12691 var isIE = Env.ie; 12692 12693 /** 12694 * Constructs a new selection instance. 12695 * 12696 * @constructor 12697 * @method Selection 12698 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. 12699 * @param {Window} win Window to bind the selection object to. 12700 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. 12701 */ 12702 function Selection(dom, win, serializer, editor) { 12703 var self = this; 12704 12705 self.dom = dom; 12706 self.win = win; 12707 self.serializer = serializer; 12708 self.editor = editor; 12709 self.bookmarkManager = new BookmarkManager(self); 12710 self.controlSelection = new ControlSelection(self, editor); 12711 12712 // No W3C Range support 12713 if (!self.win.getSelection) { 12714 self.tridentSel = new TridentSelection(self); 12715 } 12716 } 12717 12718 Selection.prototype = { 12719 /** 12720 * Move the selection cursor range to the specified node and offset. 12721 * If there is no node specified it will move it to the first suitable location within the body. 12722 * 12723 * @method setCursorLocation 12724 * @param {Node} node Optional node to put the cursor in. 12725 * @param {Number} offset Optional offset from the start of the node to put the cursor at. 12726 */ 12727 setCursorLocation: function(node, offset) { 12728 var self = this, rng = self.dom.createRng(); 12729 12730 if (!node) { 12731 self._moveEndPoint(rng, self.editor.getBody(), true); 12732 self.setRng(rng); 12733 } else { 12734 rng.setStart(node, offset); 12735 rng.setEnd(node, offset); 12736 self.setRng(rng); 12737 self.collapse(false); 12738 } 12739 }, 12740 12741 /** 12742 * Returns the selected contents using the DOM serializer passed in to this class. 12743 * 12744 * @method getContent 12745 * @param {Object} s Optional settings class with for example output format text or html. 12746 * @return {String} Selected contents in for example HTML format. 12747 * @example 12748 * // Alerts the currently selected contents 12749 * alert(tinymce.activeEditor.selection.getContent()); 12750 * 12751 * // Alerts the currently selected contents as plain text 12752 * alert(tinymce.activeEditor.selection.getContent({format: 'text'})); 12753 */ 12754 getContent: function(args) { 12755 var self = this, rng = self.getRng(), tmpElm = self.dom.create("body"); 12756 var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment; 12757 12758 args = args || {}; 12759 whiteSpaceBefore = whiteSpaceAfter = ''; 12760 args.get = true; 12761 args.format = args.format || 'html'; 12762 args.selection = true; 12763 self.editor.fire('BeforeGetContent', args); 12764 12765 if (args.format == 'text') { 12766 return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : '')); 12767 } 12768 12769 if (rng.cloneContents) { 12770 fragment = rng.cloneContents(); 12771 12772 if (fragment) { 12773 tmpElm.appendChild(fragment); 12774 } 12775 } else if (rng.item !== undefined || rng.htmlText !== undefined) { 12776 // IE will produce invalid markup if elements are present that 12777 // it doesn't understand like custom elements or HTML5 elements. 12778 // Adding a BR in front of the contents and then remoiving it seems to fix it though. 12779 tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText); 12780 tmpElm.removeChild(tmpElm.firstChild); 12781 } else { 12782 tmpElm.innerHTML = rng.toString(); 12783 } 12784 12785 // Keep whitespace before and after 12786 if (/^\s/.test(tmpElm.innerHTML)) { 12787 whiteSpaceBefore = ' '; 12788 } 12789 12790 if (/\s+$/.test(tmpElm.innerHTML)) { 12791 whiteSpaceAfter = ' '; 12792 } 12793 12794 args.getInner = true; 12795 12796 args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter; 12797 self.editor.fire('GetContent', args); 12798 12799 return args.content; 12800 }, 12801 12802 /** 12803 * Sets the current selection to the specified content. If any contents is selected it will be replaced 12804 * with the contents passed in to this function. If there is no selection the contents will be inserted 12805 * where the caret is placed in the editor/page. 12806 * 12807 * @method setContent 12808 * @param {String} content HTML contents to set could also be other formats depending on settings. 12809 * @param {Object} args Optional settings object with for example data format. 12810 * @example 12811 * // Inserts some HTML contents at the current selection 12812 * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>'); 12813 */ 12814 setContent: function(content, args) { 12815 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; 12816 12817 args = args || {format: 'html'}; 12818 args.set = true; 12819 args.selection = true; 12820 content = args.content = content; 12821 12822 // Dispatch before set content event 12823 if (!args.no_events) { 12824 self.editor.fire('BeforeSetContent', args); 12825 } 12826 12827 content = args.content; 12828 12829 if (rng.insertNode) { 12830 // Make caret marker since insertNode places the caret in the beginning of text after insert 12831 content += '<span id="__caret">_</span>'; 12832 12833 // Delete and insert new node 12834 if (rng.startContainer == doc && rng.endContainer == doc) { 12835 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents 12836 doc.body.innerHTML = content; 12837 } else { 12838 rng.deleteContents(); 12839 12840 if (doc.body.childNodes.length === 0) { 12841 doc.body.innerHTML = content; 12842 } else { 12843 // createContextualFragment doesn't exists in IE 9 DOMRanges 12844 if (rng.createContextualFragment) { 12845 rng.insertNode(rng.createContextualFragment(content)); 12846 } else { 12847 // Fake createContextualFragment call in IE 9 12848 frag = doc.createDocumentFragment(); 12849 temp = doc.createElement('div'); 12850 12851 frag.appendChild(temp); 12852 temp.outerHTML = content; 12853 12854 rng.insertNode(frag); 12855 } 12856 } 12857 } 12858 12859 // Move to caret marker 12860 caretNode = self.dom.get('__caret'); 12861 12862 // Make sure we wrap it compleatly, Opera fails with a simple select call 12863 rng = doc.createRange(); 12864 rng.setStartBefore(caretNode); 12865 rng.setEndBefore(caretNode); 12866 self.setRng(rng); 12867 12868 // Remove the caret position 12869 self.dom.remove('__caret'); 12870 12871 try { 12872 self.setRng(rng); 12873 } catch (ex) { 12874 // Might fail on Opera for some odd reason 12875 } 12876 } else { 12877 if (rng.item) { 12878 // Delete content and get caret text selection 12879 doc.execCommand('Delete', false, null); 12880 rng = self.getRng(); 12881 } 12882 12883 // Explorer removes spaces from the beginning of pasted contents 12884 if (/^\s+/.test(content)) { 12885 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content); 12886 self.dom.remove('__mce_tmp'); 12887 } else { 12888 rng.pasteHTML(content); 12889 } 12890 } 12891 12892 // Dispatch set content event 12893 if (!args.no_events) { 12894 self.editor.fire('SetContent', args); 12895 } 12896 }, 12897 12898 /** 12899 * Returns the start element of a selection range. If the start is in a text 12900 * node the parent element will be returned. 12901 * 12902 * @method getStart 12903 * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. 12904 * @return {Element} Start element of selection range. 12905 */ 12906 getStart: function(real) { 12907 var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; 12908 12909 if (rng.duplicate || rng.item) { 12910 // Control selection, return first item 12911 if (rng.item) { 12912 return rng.item(0); 12913 } 12914 12915 // Get start element 12916 checkRng = rng.duplicate(); 12917 checkRng.collapse(1); 12918 startElement = checkRng.parentElement(); 12919 if (startElement.ownerDocument !== self.dom.doc) { 12920 startElement = self.dom.getRoot(); 12921 } 12922 12923 // Check if range parent is inside the start element, then return the inner parent element 12924 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element 12925 parentElement = node = rng.parentElement(); 12926 while ((node = node.parentNode)) { 12927 if (node == startElement) { 12928 startElement = parentElement; 12929 break; 12930 } 12931 } 12932 12933 return startElement; 12934 } else { 12935 startElement = rng.startContainer; 12936 12937 if (startElement.nodeType == 1 && startElement.hasChildNodes()) { 12938 if (!real || !rng.collapsed) { 12939 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; 12940 } 12941 } 12942 12943 if (startElement && startElement.nodeType == 3) { 12944 return startElement.parentNode; 12945 } 12946 12947 return startElement; 12948 } 12949 }, 12950 12951 /** 12952 * Returns the end element of a selection range. If the end is in a text 12953 * node the parent element will be returned. 12954 * 12955 * @method getEnd 12956 * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. 12957 * @return {Element} End element of selection range. 12958 */ 12959 getEnd: function(real) { 12960 var self = this, rng = self.getRng(), endElement, endOffset; 12961 12962 if (rng.duplicate || rng.item) { 12963 if (rng.item) { 12964 return rng.item(0); 12965 } 12966 12967 rng = rng.duplicate(); 12968 rng.collapse(0); 12969 endElement = rng.parentElement(); 12970 if (endElement.ownerDocument !== self.dom.doc) { 12971 endElement = self.dom.getRoot(); 12972 } 12973 12974 if (endElement && endElement.nodeName == 'BODY') { 12975 return endElement.lastChild || endElement; 12976 } 12977 12978 return endElement; 12979 } else { 12980 endElement = rng.endContainer; 12981 endOffset = rng.endOffset; 12982 12983 if (endElement.nodeType == 1 && endElement.hasChildNodes()) { 12984 if (!real || !rng.collapsed) { 12985 endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; 12986 } 12987 } 12988 12989 if (endElement && endElement.nodeType == 3) { 12990 return endElement.parentNode; 12991 } 12992 12993 return endElement; 12994 } 12995 }, 12996 12997 /** 12998 * Returns a bookmark location for the current selection. This bookmark object 12999 * can then be used to restore the selection after some content modification to the document. 13000 * 13001 * @method getBookmark 13002 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. 13003 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. 13004 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. 13005 * @example 13006 * // Stores a bookmark of the current selection 13007 * var bm = tinymce.activeEditor.selection.getBookmark(); 13008 * 13009 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 13010 * 13011 * // Restore the selection bookmark 13012 * tinymce.activeEditor.selection.moveToBookmark(bm); 13013 */ 13014 getBookmark: function(type, normalized) { 13015 return this.bookmarkManager.getBookmark(type, normalized); 13016 }, 13017 13018 /** 13019 * Restores the selection to the specified bookmark. 13020 * 13021 * @method moveToBookmark 13022 * @param {Object} bookmark Bookmark to restore selection from. 13023 * @return {Boolean} true/false if it was successful or not. 13024 * @example 13025 * // Stores a bookmark of the current selection 13026 * var bm = tinymce.activeEditor.selection.getBookmark(); 13027 * 13028 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 13029 * 13030 * // Restore the selection bookmark 13031 * tinymce.activeEditor.selection.moveToBookmark(bm); 13032 */ 13033 moveToBookmark: function(bookmark) { 13034 return this.bookmarkManager.moveToBookmark(bookmark); 13035 }, 13036 13037 /** 13038 * Selects the specified element. This will place the start and end of the selection range around the element. 13039 * 13040 * @method select 13041 * @param {Element} node HMTL DOM element to select. 13042 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. 13043 * @return {Element} Selected element the same element as the one that got passed in. 13044 * @example 13045 * // Select the first paragraph in the active editor 13046 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 13047 */ 13048 select: function(node, content) { 13049 var self = this, dom = self.dom, rng = dom.createRng(), idx; 13050 13051 // Clear stored range set by FocusManager 13052 self.lastFocusBookmark = null; 13053 13054 if (node) { 13055 if (!content && self.controlSelection.controlSelect(node)) { 13056 return; 13057 } 13058 13059 idx = dom.nodeIndex(node); 13060 rng.setStart(node.parentNode, idx); 13061 rng.setEnd(node.parentNode, idx + 1); 13062 13063 // Find first/last text node or BR element 13064 if (content) { 13065 self._moveEndPoint(rng, node, true); 13066 self._moveEndPoint(rng, node); 13067 } 13068 13069 self.setRng(rng); 13070 } 13071 13072 return node; 13073 }, 13074 13075 /** 13076 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. 13077 * 13078 * @method isCollapsed 13079 * @return {Boolean} true/false state if the selection range is collapsed or not. 13080 * Collapsed means if it's a caret or a larger selection. 13081 */ 13082 isCollapsed: function() { 13083 var self = this, rng = self.getRng(), sel = self.getSel(); 13084 13085 if (!rng || rng.item) { 13086 return false; 13087 } 13088 13089 if (rng.compareEndPoints) { 13090 return rng.compareEndPoints('StartToEnd', rng) === 0; 13091 } 13092 13093 return !sel || rng.collapsed; 13094 }, 13095 13096 /** 13097 * Collapse the selection to start or end of range. 13098 * 13099 * @method collapse 13100 * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to start. 13101 */ 13102 collapse: function(toStart) { 13103 var self = this, rng = self.getRng(), node; 13104 13105 // Control range on IE 13106 if (rng.item) { 13107 node = rng.item(0); 13108 rng = self.win.document.body.createTextRange(); 13109 rng.moveToElementText(node); 13110 } 13111 13112 rng.collapse(!!toStart); 13113 self.setRng(rng); 13114 }, 13115 13116 /** 13117 * Returns the browsers internal selection object. 13118 * 13119 * @method getSel 13120 * @return {Selection} Internal browser selection object. 13121 */ 13122 getSel: function() { 13123 var win = this.win; 13124 13125 return win.getSelection ? win.getSelection() : win.document.selection; 13126 }, 13127 13128 /** 13129 * Returns the browsers internal range object. 13130 * 13131 * @method getRng 13132 * @param {Boolean} w3c Forces a compatible W3C range on IE. 13133 * @return {Range} Internal browser range object. 13134 * @see http://www.quirksmode.org/dom/range_intro.html 13135 * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ 13136 */ 13137 getRng: function(w3c) { 13138 var self = this, selection, rng, elm, doc = self.win.document, ieRng; 13139 13140 function tryCompareBoundaryPoints(how, sourceRange, destinationRange) { 13141 try { 13142 return sourceRange.compareBoundaryPoints(how, destinationRange); 13143 } catch (ex) { 13144 // Gecko throws wrong document exception if the range points 13145 // to nodes that where removed from the dom #6690 13146 // Browsers should mutate existing DOMRange instances so that they always point 13147 // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink 13148 // For performance reasons just return -1 13149 return -1; 13150 } 13151 } 13152 13153 // Use last rng passed from FocusManager if it's available this enables 13154 // calls to editor.selection.getStart() to work when caret focus is lost on IE 13155 if (!w3c && self.lastFocusBookmark) { 13156 var bookmark = self.lastFocusBookmark; 13157 13158 // Convert bookmark to range IE 11 fix 13159 if (bookmark.startContainer) { 13160 rng = doc.createRange(); 13161 rng.setStart(bookmark.startContainer, bookmark.startOffset); 13162 rng.setEnd(bookmark.endContainer, bookmark.endOffset); 13163 } else { 13164 rng = bookmark; 13165 } 13166 13167 return rng; 13168 } 13169 13170 // Found tridentSel object then we need to use that one 13171 if (w3c && self.tridentSel) { 13172 return self.tridentSel.getRangeAt(0); 13173 } 13174 13175 try { 13176 if ((selection = self.getSel())) { 13177 if (selection.rangeCount > 0) { 13178 rng = selection.getRangeAt(0); 13179 } else { 13180 rng = selection.createRange ? selection.createRange() : doc.createRange(); 13181 } 13182 } 13183 } catch (ex) { 13184 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 13185 } 13186 13187 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet 13188 // IE 11 doesn't support the selection object so we check for that as well 13189 if (isIE && rng && rng.setStart && doc.selection) { 13190 try { 13191 // IE will sometimes throw an exception here 13192 ieRng = doc.selection.createRange(); 13193 } catch (ex) { 13194 13195 } 13196 13197 if (ieRng && ieRng.item) { 13198 elm = ieRng.item(0); 13199 rng = doc.createRange(); 13200 rng.setStartBefore(elm); 13201 rng.setEndAfter(elm); 13202 } 13203 } 13204 13205 // No range found then create an empty one 13206 // This can occur when the editor is placed in a hidden container element on Gecko 13207 // Or on IE when there was an exception 13208 if (!rng) { 13209 rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); 13210 } 13211 13212 // If range is at start of document then move it to start of body 13213 if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { 13214 elm = self.dom.getRoot(); 13215 rng.setStart(elm, 0); 13216 rng.setEnd(elm, 0); 13217 } 13218 13219 if (self.selectedRange && self.explicitRange) { 13220 if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 && 13221 tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) { 13222 // Safari, Opera and Chrome only ever select text which causes the range to change. 13223 // This lets us use the originally set range if the selection hasn't been changed by the user. 13224 rng = self.explicitRange; 13225 } else { 13226 self.selectedRange = null; 13227 self.explicitRange = null; 13228 } 13229 } 13230 13231 return rng; 13232 }, 13233 13234 /** 13235 * Changes the selection to the specified DOM range. 13236 * 13237 * @method setRng 13238 * @param {Range} rng Range to select. 13239 */ 13240 setRng: function(rng, forward) { 13241 var self = this, sel; 13242 13243 if (!rng) { 13244 return; 13245 } 13246 13247 // Is IE specific range 13248 if (rng.select) { 13249 try { 13250 rng.select(); 13251 } catch (ex) { 13252 // Needed for some odd IE bug #1843306 13253 } 13254 13255 return; 13256 } 13257 13258 if (!self.tridentSel) { 13259 sel = self.getSel(); 13260 13261 if (sel) { 13262 self.explicitRange = rng; 13263 13264 try { 13265 sel.removeAllRanges(); 13266 sel.addRange(rng); 13267 } catch (ex) { 13268 // IE might throw errors here if the editor is within a hidden container and selection is changed 13269 } 13270 13271 // Forward is set to false and we have an extend function 13272 if (forward === false && sel.extend) { 13273 sel.collapse(rng.endContainer, rng.endOffset); 13274 sel.extend(rng.startContainer, rng.startOffset); 13275 } 13276 13277 // adding range isn't always successful so we need to check range count otherwise an exception can occur 13278 self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null; 13279 } 13280 } else { 13281 // Is W3C Range fake range on IE 13282 if (rng.cloneRange) { 13283 try { 13284 self.tridentSel.addRange(rng); 13285 return; 13286 } catch (ex) { 13287 //IE9 throws an error here if called before selection is placed in the editor 13288 } 13289 } 13290 } 13291 }, 13292 13293 /** 13294 * Sets the current selection to the specified DOM element. 13295 * 13296 * @method setNode 13297 * @param {Element} elm Element to set as the contents of the selection. 13298 * @return {Element} Returns the element that got passed in. 13299 * @example 13300 * // Inserts a DOM node at current selection/caret location 13301 * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'})); 13302 */ 13303 setNode: function(elm) { 13304 var self = this; 13305 13306 self.setContent(self.dom.getOuterHTML(elm)); 13307 13308 return elm; 13309 }, 13310 13311 /** 13312 * Returns the currently selected element or the common ancestor element for both start and end of the selection. 13313 * 13314 * @method getNode 13315 * @return {Element} Currently selected element or common ancestor element. 13316 * @example 13317 * // Alerts the currently selected elements node name 13318 * alert(tinymce.activeEditor.selection.getNode().nodeName); 13319 */ 13320 getNode: function() { 13321 var self = this, rng = self.getRng(), elm; 13322 var startContainer = rng.startContainer, endContainer = rng.endContainer; 13323 var startOffset = rng.startOffset, endOffset = rng.endOffset, root = self.dom.getRoot(); 13324 13325 function skipEmptyTextNodes(node, forwards) { 13326 var orig = node; 13327 13328 while (node && node.nodeType === 3 && node.length === 0) { 13329 node = forwards ? node.nextSibling : node.previousSibling; 13330 } 13331 13332 return node || orig; 13333 } 13334 13335 // Range maybe lost after the editor is made visible again 13336 if (!rng) { 13337 return root; 13338 } 13339 13340 if (rng.setStart) { 13341 elm = rng.commonAncestorContainer; 13342 13343 // Handle selection a image or other control like element such as anchors 13344 if (!rng.collapsed) { 13345 if (startContainer == endContainer) { 13346 if (endOffset - startOffset < 2) { 13347 if (startContainer.hasChildNodes()) { 13348 elm = startContainer.childNodes[startOffset]; 13349 } 13350 } 13351 } 13352 13353 // If the anchor node is a element instead of a text node then return this element 13354 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 13355 // return sel.anchorNode.childNodes[sel.anchorOffset]; 13356 13357 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 13358 // This happens when you double click an underlined word in FireFox. 13359 if (startContainer.nodeType === 3 && endContainer.nodeType === 3) { 13360 if (startContainer.length === startOffset) { 13361 startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); 13362 } else { 13363 startContainer = startContainer.parentNode; 13364 } 13365 13366 if (endOffset === 0) { 13367 endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); 13368 } else { 13369 endContainer = endContainer.parentNode; 13370 } 13371 13372 if (startContainer && startContainer === endContainer) { 13373 return startContainer; 13374 } 13375 } 13376 } 13377 13378 if (elm && elm.nodeType == 3) { 13379 return elm.parentNode; 13380 } 13381 13382 return elm; 13383 } 13384 13385 elm = rng.item ? rng.item(0) : rng.parentElement(); 13386 13387 // IE 7 might return elements outside the iframe 13388 if (elm.ownerDocument !== self.win.document) { 13389 elm = root; 13390 } 13391 13392 return elm; 13393 }, 13394 13395 getSelectedBlocks: function(startElm, endElm) { 13396 var self = this, dom = self.dom, node, root, selectedBlocks = []; 13397 13398 root = dom.getRoot(); 13399 startElm = dom.getParent(startElm || self.getStart(), dom.isBlock); 13400 endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock); 13401 13402 if (startElm && startElm != root) { 13403 selectedBlocks.push(startElm); 13404 } 13405 13406 if (startElm && endElm && startElm != endElm) { 13407 node = startElm; 13408 13409 var walker = new TreeWalker(startElm, root); 13410 while ((node = walker.next()) && node != endElm) { 13411 if (dom.isBlock(node)) { 13412 selectedBlocks.push(node); 13413 } 13414 } 13415 } 13416 13417 if (endElm && startElm != endElm && endElm != root) { 13418 selectedBlocks.push(endElm); 13419 } 13420 13421 return selectedBlocks; 13422 }, 13423 13424 isForward: function() { 13425 var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; 13426 13427 // No support for selection direction then always return true 13428 if (!sel || !sel.anchorNode || !sel.focusNode) { 13429 return true; 13430 } 13431 13432 anchorRange = dom.createRng(); 13433 anchorRange.setStart(sel.anchorNode, sel.anchorOffset); 13434 anchorRange.collapse(true); 13435 13436 focusRange = dom.createRng(); 13437 focusRange.setStart(sel.focusNode, sel.focusOffset); 13438 focusRange.collapse(true); 13439 13440 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 13441 }, 13442 13443 normalize: function() { 13444 var self = this, rng = self.getRng(); 13445 13446 if (!isIE && new RangeUtils(self.dom).normalize(rng)) { 13447 self.setRng(rng, self.isForward()); 13448 } 13449 13450 return rng; 13451 }, 13452 13453 /** 13454 * Executes callback of the current selection matches the specified selector or not and passes the state and args to the callback. 13455 * 13456 * @method selectorChanged 13457 * @param {String} selector CSS selector to check for. 13458 * @param {function} callback Callback with state and args when the selector is matches or not. 13459 */ 13460 selectorChanged: function(selector, callback) { 13461 var self = this, currentSelectors; 13462 13463 if (!self.selectorChangedData) { 13464 self.selectorChangedData = {}; 13465 currentSelectors = {}; 13466 13467 self.editor.on('NodeChange', function(e) { 13468 var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; 13469 13470 // Check for new matching selectors 13471 each(self.selectorChangedData, function(callbacks, selector) { 13472 each(parents, function(node) { 13473 if (dom.is(node, selector)) { 13474 if (!currentSelectors[selector]) { 13475 // Execute callbacks 13476 each(callbacks, function(callback) { 13477 callback(true, {node: node, selector: selector, parents: parents}); 13478 }); 13479 13480 currentSelectors[selector] = callbacks; 13481 } 13482 13483 matchedSelectors[selector] = callbacks; 13484 return false; 13485 } 13486 }); 13487 }); 13488 13489 // Check if current selectors still match 13490 each(currentSelectors, function(callbacks, selector) { 13491 if (!matchedSelectors[selector]) { 13492 delete currentSelectors[selector]; 13493 13494 each(callbacks, function(callback) { 13495 callback(false, {node: node, selector: selector, parents: parents}); 13496 }); 13497 } 13498 }); 13499 }); 13500 } 13501 13502 // Add selector listeners 13503 if (!self.selectorChangedData[selector]) { 13504 self.selectorChangedData[selector] = []; 13505 } 13506 13507 self.selectorChangedData[selector].push(callback); 13508 13509 return self; 13510 }, 13511 13512 getScrollContainer: function() { 13513 var scrollContainer, node = this.dom.getRoot(); 13514 13515 while (node && node.nodeName != 'BODY') { 13516 if (node.scrollHeight > node.clientHeight) { 13517 scrollContainer = node; 13518 break; 13519 } 13520 13521 node = node.parentNode; 13522 } 13523 13524 return scrollContainer; 13525 }, 13526 13527 scrollIntoView: function(elm) { 13528 var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH; 13529 13530 function getPos(elm) { 13531 var x = 0, y = 0; 13532 13533 var offsetParent = elm; 13534 while (offsetParent && offsetParent.nodeType) { 13535 x += offsetParent.offsetLeft || 0; 13536 y += offsetParent.offsetTop || 0; 13537 offsetParent = offsetParent.offsetParent; 13538 } 13539 13540 return {x: x, y: y}; 13541 } 13542 13543 if (root.nodeName != 'BODY') { 13544 var scrollContainer = self.getScrollContainer(); 13545 if (scrollContainer) { 13546 y = getPos(elm).y - getPos(scrollContainer).y; 13547 viewPortH = scrollContainer.clientHeight; 13548 viewPortY = scrollContainer.scrollTop; 13549 if (y < viewPortY || y + 25 > viewPortY + viewPortH) { 13550 scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25; 13551 } 13552 13553 return; 13554 } 13555 } 13556 13557 viewPort = dom.getViewPort(self.editor.getWin()); 13558 y = dom.getPos(elm).y; 13559 viewPortY = viewPort.y; 13560 viewPortH = viewPort.h; 13561 if (y < viewPort.y || y + 25 > viewPortY + viewPortH) { 13562 self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25); 13563 } 13564 }, 13565 13566 placeCaretAt: function(clientX, clientY) { 13567 var doc = this.editor.getDoc(), rng, point; 13568 13569 if (doc.caretPositionFromPoint) { 13570 point = doc.caretPositionFromPoint(clientX, clientY); 13571 rng = doc.createRange(); 13572 rng.setStart(point.offsetNode, point.offset); 13573 rng.collapse(true); 13574 } else if (doc.caretRangeFromPoint) { 13575 rng = doc.caretRangeFromPoint(clientX, clientY); 13576 } else if (doc.body.createTextRange) { 13577 rng = doc.body.createTextRange(); 13578 13579 try { 13580 rng.moveToPoint(clientX, clientY); 13581 rng.collapse(true); 13582 } catch (ex) { 13583 rng.collapse(clientY < doc.body.clientHeight); 13584 } 13585 } 13586 13587 this.setRng(rng); 13588 }, 13589 13590 _moveEndPoint: function(rng, node, start) { 13591 var root = node, walker = new TreeWalker(node, root); 13592 var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements(); 13593 13594 do { 13595 // Text node 13596 if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) { 13597 if (start) { 13598 rng.setStart(node, 0); 13599 } else { 13600 rng.setEnd(node, node.nodeValue.length); 13601 } 13602 13603 return; 13604 } 13605 13606 // BR/IMG/INPUT elements but not table cells 13607 if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) { 13608 if (start) { 13609 rng.setStartBefore(node); 13610 } else { 13611 if (node.nodeName == 'BR') { 13612 rng.setEndBefore(node); 13613 } else { 13614 rng.setEndAfter(node); 13615 } 13616 } 13617 13618 return; 13619 } 13620 13621 // Found empty text block old IE can place the selection inside those 13622 if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) { 13623 if (start) { 13624 rng.setStart(node, 0); 13625 } else { 13626 rng.setEnd(node, 0); 13627 } 13628 13629 return; 13630 } 13631 } while ((node = (start ? walker.next() : walker.prev()))); 13632 13633 // Failed to find any text node or other suitable location then move to the root of body 13634 if (root.nodeName == 'BODY') { 13635 if (start) { 13636 rng.setStart(root, 0); 13637 } else { 13638 rng.setEnd(root, root.childNodes.length); 13639 } 13640 } 13641 }, 13642 13643 destroy: function() { 13644 this.win = null; 13645 this.controlSelection.destroy(); 13646 } 13647 }; 13648 13649 return Selection; 13650 }); 13651 13652 // Included from: js/tinymce/classes/dom/ElementUtils.js 13653 13654 /** 13655 * ElementUtils.js 13656 * 13657 * Copyright, Moxiecode Systems AB 13658 * Released under LGPL License. 13659 * 13660 * License: http://www.tinymce.com/license 13661 * Contributing: http://www.tinymce.com/contributing 13662 */ 13663 13664 /** 13665 * Utility class for various element specific functions. 13666 * 13667 * @private 13668 */ 13669 define("tinymce/dom/ElementUtils", [ 13670 "tinymce/dom/BookmarkManager", 13671 "tinymce/util/Tools" 13672 ], function(BookmarkManager, Tools) { 13673 var each = Tools.each; 13674 13675 function ElementUtils(dom) { 13676 /** 13677 * Compares two nodes and checks if it's attributes and styles matches. 13678 * This doesn't compare classes as items since their order is significant. 13679 * 13680 * @method compare 13681 * @param {Node} node1 First node to compare with. 13682 * @param {Node} node2 Second node to compare with. 13683 * @return {boolean} True/false if the nodes are the same or not. 13684 */ 13685 this.compare = function(node1, node2) { 13686 // Not the same name 13687 if (node1.nodeName != node2.nodeName) { 13688 return false; 13689 } 13690 13691 /** 13692 * Returns all the nodes attributes excluding internal ones, styles and classes. 13693 * 13694 * @private 13695 * @param {Node} node Node to get attributes from. 13696 * @return {Object} Name/value object with attributes and attribute values. 13697 */ 13698 function getAttribs(node) { 13699 var attribs = {}; 13700 13701 each(dom.getAttribs(node), function(attr) { 13702 var name = attr.nodeName.toLowerCase(); 13703 13704 // Don't compare internal attributes or style 13705 if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') { 13706 attribs[name] = dom.getAttrib(node, name); 13707 } 13708 }); 13709 13710 return attribs; 13711 } 13712 13713 /** 13714 * Compares two objects checks if it's key + value exists in the other one. 13715 * 13716 * @private 13717 * @param {Object} obj1 First object to compare. 13718 * @param {Object} obj2 Second object to compare. 13719 * @return {boolean} True/false if the objects matches or not. 13720 */ 13721 function compareObjects(obj1, obj2) { 13722 var value, name; 13723 13724 for (name in obj1) { 13725 // Obj1 has item obj2 doesn't have 13726 if (obj1.hasOwnProperty(name)) { 13727 value = obj2[name]; 13728 13729 // Obj2 doesn't have obj1 item 13730 if (typeof value == "undefined") { 13731 return false; 13732 } 13733 13734 // Obj2 item has a different value 13735 if (obj1[name] != value) { 13736 return false; 13737 } 13738 13739 // Delete similar value 13740 delete obj2[name]; 13741 } 13742 } 13743 13744 // Check if obj 2 has something obj 1 doesn't have 13745 for (name in obj2) { 13746 // Obj2 has item obj1 doesn't have 13747 if (obj2.hasOwnProperty(name)) { 13748 return false; 13749 } 13750 } 13751 13752 return true; 13753 } 13754 13755 // Attribs are not the same 13756 if (!compareObjects(getAttribs(node1), getAttribs(node2))) { 13757 return false; 13758 } 13759 13760 // Styles are not the same 13761 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { 13762 return false; 13763 } 13764 13765 return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2); 13766 }; 13767 } 13768 13769 return ElementUtils; 13770 }); 13771 13772 // Included from: js/tinymce/classes/fmt/Preview.js 13773 13774 /** 13775 * Preview.js 13776 * 13777 * Copyright, Moxiecode Systems AB 13778 * Released under LGPL License. 13779 * 13780 * License: http://www.tinymce.com/license 13781 * Contributing: http://www.tinymce.com/contributing 13782 */ 13783 13784 /** 13785 * Internal class for generating previews styles for formats. 13786 * 13787 * Example: 13788 * Preview.getCssText(editor, 'bold'); 13789 * 13790 * @class tinymce.fmt.Preview 13791 * @private 13792 */ 13793 define("tinymce/fmt/Preview", [ 13794 "tinymce/util/Tools" 13795 ], function(Tools) { 13796 var each = Tools.each; 13797 13798 function getCssText(editor, format) { 13799 var name, previewElm, dom = editor.dom; 13800 var previewCss = '', parentFontSize, previewStyles; 13801 13802 previewStyles = editor.settings.preview_styles; 13803 13804 // No preview forced 13805 if (previewStyles === false) { 13806 return ''; 13807 } 13808 13809 // Default preview 13810 if (!previewStyles) { 13811 previewStyles = 'font-family font-size font-weight font-style text-decoration ' + 13812 'text-transform color background-color border border-radius outline text-shadow'; 13813 } 13814 13815 // Removes any variables since these can't be previewed 13816 function removeVars(val) { 13817 return val.replace(/%(\w+)/g, ''); 13818 } 13819 13820 // Create block/inline element to use for preview 13821 if (typeof(format) == "string") { 13822 format = editor.formatter.get(format); 13823 if (!format) { 13824 return; 13825 } 13826 13827 format = format[0]; 13828 } 13829 13830 name = format.block || format.inline || 'span'; 13831 previewElm = dom.create(name); 13832 13833 // Add format styles to preview element 13834 each(format.styles, function(value, name) { 13835 value = removeVars(value); 13836 13837 if (value) { 13838 dom.setStyle(previewElm, name, value); 13839 } 13840 }); 13841 13842 // Add attributes to preview element 13843 each(format.attributes, function(value, name) { 13844 value = removeVars(value); 13845 13846 if (value) { 13847 dom.setAttrib(previewElm, name, value); 13848 } 13849 }); 13850 13851 // Add classes to preview element 13852 each(format.classes, function(value) { 13853 value = removeVars(value); 13854 13855 if (!dom.hasClass(previewElm, value)) { 13856 dom.addClass(previewElm, value); 13857 } 13858 }); 13859 13860 editor.fire('PreviewFormats'); 13861 13862 // Add the previewElm outside the visual area 13863 dom.setStyles(previewElm, {position: 'absolute', left: -0xFFFF}); 13864 editor.getBody().appendChild(previewElm); 13865 13866 // Get parent container font size so we can compute px values out of em/% for older IE:s 13867 parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); 13868 parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; 13869 13870 each(previewStyles.split(' '), function(name) { 13871 var value = dom.getStyle(previewElm, name, true); 13872 13873 // If background is transparent then check if the body has a background color we can use 13874 if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { 13875 value = dom.getStyle(editor.getBody(), name, true); 13876 13877 // Ignore white since it's the default color, not the nicest fix 13878 // TODO: Fix this by detecting runtime style 13879 if (dom.toHex(value).toLowerCase() == '#ffffff') { 13880 return; 13881 } 13882 } 13883 13884 if (name == 'color') { 13885 // Ignore black since it's the default color, not the nicest fix 13886 // TODO: Fix this by detecting runtime style 13887 if (dom.toHex(value).toLowerCase() == '#000000') { 13888 return; 13889 } 13890 } 13891 13892 // Old IE won't calculate the font size so we need to do that manually 13893 if (name == 'font-size') { 13894 if (/em|%$/.test(value)) { 13895 if (parentFontSize === 0) { 13896 return; 13897 } 13898 13899 // Convert font size from em/% to px 13900 value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); 13901 value = (value * parentFontSize) + 'px'; 13902 } 13903 } 13904 13905 if (name == "border" && value) { 13906 previewCss += 'padding:0 2px;'; 13907 } 13908 13909 previewCss += name + ':' + value + ';'; 13910 }); 13911 13912 editor.fire('AfterPreviewFormats'); 13913 13914 //previewCss += 'line-height:normal'; 13915 13916 dom.remove(previewElm); 13917 13918 return previewCss; 13919 } 13920 13921 return { 13922 getCssText: getCssText 13923 }; 13924 }); 13925 13926 // Included from: js/tinymce/classes/Formatter.js 13927 13928 /** 13929 * Formatter.js 13930 * 13931 * Copyright, Moxiecode Systems AB 13932 * Released under LGPL License. 13933 * 13934 * License: http://www.tinymce.com/license 13935 * Contributing: http://www.tinymce.com/contributing 13936 */ 13937 13938 /** 13939 * Text formatter engine class. This class is used to apply formats like bold, italic, font size 13940 * etc to the current selection or specific nodes. This engine was build to replace the browsers 13941 * default formatting logic for execCommand due to it's inconsistent and buggy behavior. 13942 * 13943 * @class tinymce.Formatter 13944 * @example 13945 * tinymce.activeEditor.formatter.register('mycustomformat', { 13946 * inline: 'span', 13947 * styles: {color: '#ff0000'} 13948 * }); 13949 * 13950 * tinymce.activeEditor.formatter.apply('mycustomformat'); 13951 */ 13952 define("tinymce/Formatter", [ 13953 "tinymce/dom/TreeWalker", 13954 "tinymce/dom/RangeUtils", 13955 "tinymce/dom/BookmarkManager", 13956 "tinymce/dom/ElementUtils", 13957 "tinymce/util/Tools", 13958 "tinymce/fmt/Preview" 13959 ], function(TreeWalker, RangeUtils, BookmarkManager, ElementUtils, Tools, Preview) { 13960 /** 13961 * Constructs a new formatter instance. 13962 * 13963 * @constructor Formatter 13964 * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. 13965 */ 13966 return function(ed) { 13967 var formats = {}, 13968 dom = ed.dom, 13969 selection = ed.selection, 13970 rangeUtils = new RangeUtils(dom), 13971 isValid = ed.schema.isValidChild, 13972 isBlock = dom.isBlock, 13973 forcedRootBlock = ed.settings.forced_root_block, 13974 nodeIndex = dom.nodeIndex, 13975 INVISIBLE_CHAR = '\uFEFF', 13976 MCE_ATTR_RE = /^(src|href|style)$/, 13977 FALSE = false, 13978 TRUE = true, 13979 formatChangeData, 13980 undef, 13981 getContentEditable = dom.getContentEditable, 13982 disableCaretContainer, 13983 markCaretContainersBogus, 13984 isBookmarkNode = BookmarkManager.isBookmarkNode; 13985 13986 var each = Tools.each, 13987 grep = Tools.grep, 13988 walk = Tools.walk, 13989 extend = Tools.extend; 13990 13991 function isTextBlock(name) { 13992 if (name.nodeType) { 13993 name = name.nodeName; 13994 } 13995 13996 return !!ed.schema.getTextBlockElements()[name.toLowerCase()]; 13997 } 13998 13999 function getParents(node, selector) { 14000 return dom.getParents(node, selector, dom.getRoot()); 14001 } 14002 14003 function isCaretNode(node) { 14004 return node.nodeType === 1 && node.id === '_mce_caret'; 14005 } 14006 14007 function defaultFormats() { 14008 register({ 14009 valigntop: [ 14010 {selector: 'td,th', styles: {'verticalAlign': 'top'}} 14011 ], 14012 14013 valignmiddle: [ 14014 {selector: 'td,th', styles: {'verticalAlign': 'middle'}} 14015 ], 14016 14017 valignbottom: [ 14018 {selector: 'td,th', styles: {'verticalAlign': 'bottom'}} 14019 ], 14020 14021 alignleft: [ 14022 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'}, 14023 {selector: 'img,table', collapsed: false, styles: {'float': 'left'}} 14024 ], 14025 14026 aligncenter: [ 14027 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'}, 14028 {selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}}, 14029 {selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}} 14030 ], 14031 14032 alignright: [ 14033 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'}, 14034 {selector: 'img,table', collapsed: false, styles: {'float': 'right'}} 14035 ], 14036 14037 alignjustify: [ 14038 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'} 14039 ], 14040 14041 bold: [ 14042 {inline: 'strong', remove: 'all'}, 14043 {inline: 'span', styles: {fontWeight: 'bold'}}, 14044 {inline: 'b', remove: 'all'} 14045 ], 14046 14047 italic: [ 14048 {inline: 'em', remove: 'all'}, 14049 {inline: 'span', styles: {fontStyle: 'italic'}}, 14050 {inline: 'i', remove: 'all'} 14051 ], 14052 14053 underline: [ 14054 {inline: 'span', styles: {textDecoration: 'underline'}, exact: true}, 14055 {inline: 'u', remove: 'all'} 14056 ], 14057 14058 strikethrough: [ 14059 {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true}, 14060 {inline: 'strike', remove: 'all'} 14061 ], 14062 14063 forecolor: {inline: 'span', styles: {color: '%value'}, links: true, remove_similar: true}, 14064 hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, links: true, remove_similar: true}, 14065 fontname: {inline: 'span', styles: {fontFamily: '%value'}}, 14066 fontsize: {inline: 'span', styles: {fontSize: '%value'}}, 14067 fontsize_class: {inline: 'span', attributes: {'class': '%value'}}, 14068 blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'}, 14069 subscript: {inline: 'sub'}, 14070 superscript: {inline: 'sup'}, 14071 code: {inline: 'code'}, 14072 14073 link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, 14074 onmatch: function() { 14075 return true; 14076 }, 14077 14078 onformat: function(elm, fmt, vars) { 14079 each(vars, function(value, key) { 14080 dom.setAttrib(elm, key, value); 14081 }); 14082 } 14083 }, 14084 14085 removeformat: [ 14086 { 14087 selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q', 14088 remove: 'all', 14089 split: true, 14090 expand: false, 14091 block_expand: true, 14092 deep: true 14093 }, 14094 {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true}, 14095 {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true} 14096 ] 14097 }); 14098 14099 // Register default block formats 14100 each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) { 14101 register(name, {block: name, remove: 'all'}); 14102 }); 14103 14104 // Register user defined formats 14105 register(ed.settings.formats); 14106 } 14107 14108 function addKeyboardShortcuts() { 14109 // Add some inline shortcuts 14110 ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); 14111 ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); 14112 ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); 14113 14114 // BlockFormat shortcuts keys 14115 for (var i = 1; i <= 6; i++) { 14116 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); 14117 } 14118 14119 ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); 14120 ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); 14121 ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); 14122 } 14123 14124 // Public functions 14125 14126 /** 14127 * Returns the format by name or all formats if no name is specified. 14128 * 14129 * @method get 14130 * @param {String} name Optional name to retrive by. 14131 * @return {Array/Object} Array/Object with all registred formats or a specific format. 14132 */ 14133 function get(name) { 14134 return name ? formats[name] : formats; 14135 } 14136 14137 /** 14138 * Registers a specific format by name. 14139 * 14140 * @method register 14141 * @param {Object/String} name Name of the format for example "bold". 14142 * @param {Object/Array} format Optional format object or array of format variants 14143 * can only be omitted if the first arg is an object. 14144 */ 14145 function register(name, format) { 14146 if (name) { 14147 if (typeof(name) !== 'string') { 14148 each(name, function(format, name) { 14149 register(name, format); 14150 }); 14151 } else { 14152 // Force format into array and add it to internal collection 14153 format = format.length ? format : [format]; 14154 14155 each(format, function(format) { 14156 // Set deep to false by default on selector formats this to avoid removing 14157 // alignment on images inside paragraphs when alignment is changed on paragraphs 14158 if (format.deep === undef) { 14159 format.deep = !format.selector; 14160 } 14161 14162 // Default to true 14163 if (format.split === undef) { 14164 format.split = !format.selector || format.inline; 14165 } 14166 14167 // Default to true 14168 if (format.remove === undef && format.selector && !format.inline) { 14169 format.remove = 'none'; 14170 } 14171 14172 // Mark format as a mixed format inline + block level 14173 if (format.selector && format.inline) { 14174 format.mixed = true; 14175 format.block_expand = true; 14176 } 14177 14178 // Split classes if needed 14179 if (typeof(format.classes) === 'string') { 14180 format.classes = format.classes.split(/\s+/); 14181 } 14182 }); 14183 14184 formats[name] = format; 14185 } 14186 } 14187 } 14188 14189 /** 14190 * Unregister a specific format by name. 14191 * 14192 * @method unregister 14193 * @param {String} name Name of the format for example "bold". 14194 */ 14195 function unregister(name) { 14196 if (name && formats[name]) { 14197 delete formats[name]; 14198 } 14199 14200 return formats; 14201 } 14202 14203 function getTextDecoration(node) { 14204 var decoration; 14205 14206 ed.dom.getParent(node, function(n) { 14207 decoration = ed.dom.getStyle(n, 'text-decoration'); 14208 return decoration && decoration !== 'none'; 14209 }); 14210 14211 return decoration; 14212 } 14213 14214 function processUnderlineAndColor(node) { 14215 var textDecoration; 14216 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { 14217 textDecoration = getTextDecoration(node.parentNode); 14218 if (ed.dom.getStyle(node, 'color') && textDecoration) { 14219 ed.dom.setStyle(node, 'text-decoration', textDecoration); 14220 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { 14221 ed.dom.setStyle(node, 'text-decoration', null); 14222 } 14223 } 14224 } 14225 14226 /** 14227 * Applies the specified format to the current selection or specified node. 14228 * 14229 * @method apply 14230 * @param {String} name Name of format to apply. 14231 * @param {Object} vars Optional list of variables to replace within format before applying it. 14232 * @param {Node} node Optional node to apply the format to defaults to current selection. 14233 */ 14234 function apply(name, vars, node) { 14235 var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed(); 14236 14237 function setElementFormat(elm, fmt) { 14238 fmt = fmt || format; 14239 14240 if (elm) { 14241 if (fmt.onformat) { 14242 fmt.onformat(elm, fmt, vars, node); 14243 } 14244 14245 each(fmt.styles, function(value, name) { 14246 dom.setStyle(elm, name, replaceVars(value, vars)); 14247 }); 14248 14249 // Needed for the WebKit span spam bug 14250 // TODO: Remove this once WebKit/Blink fixes this 14251 if (fmt.styles) { 14252 var styleVal = dom.getAttrib(elm, 'style'); 14253 14254 if (styleVal) { 14255 elm.setAttribute('data-mce-style', styleVal); 14256 } 14257 } 14258 14259 each(fmt.attributes, function(value, name) { 14260 dom.setAttrib(elm, name, replaceVars(value, vars)); 14261 }); 14262 14263 each(fmt.classes, function(value) { 14264 value = replaceVars(value, vars); 14265 14266 if (!dom.hasClass(elm, value)) { 14267 dom.addClass(elm, value); 14268 } 14269 }); 14270 } 14271 } 14272 14273 function adjustSelectionToVisibleSelection() { 14274 function findSelectionEnd(start, end) { 14275 var walker = new TreeWalker(end); 14276 for (node = walker.current(); node; node = walker.prev()) { 14277 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { 14278 return node; 14279 } 14280 } 14281 } 14282 14283 // Adjust selection so that a end container with a end offset of zero is not included in the selection 14284 // as this isn't visible to the user. 14285 var rng = ed.selection.getRng(); 14286 var start = rng.startContainer; 14287 var end = rng.endContainer; 14288 14289 if (start != end && rng.endOffset === 0) { 14290 var newEnd = findSelectionEnd(start, end); 14291 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; 14292 14293 rng.setEnd(newEnd, endOffset); 14294 } 14295 14296 return rng; 14297 } 14298 14299 function applyRngStyle(rng, bookmark, node_specific) { 14300 var newWrappers = [], wrapName, wrapElm, contentEditable = true; 14301 14302 // Setup wrapper element 14303 wrapName = format.inline || format.block; 14304 wrapElm = dom.create(wrapName); 14305 setElementFormat(wrapElm); 14306 14307 rangeUtils.walk(rng, function(nodes) { 14308 var currentWrapElm; 14309 14310 /** 14311 * Process a list of nodes wrap them. 14312 */ 14313 function process(node) { 14314 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; 14315 14316 lastContentEditable = contentEditable; 14317 nodeName = node.nodeName.toLowerCase(); 14318 parentName = node.parentNode.nodeName.toLowerCase(); 14319 14320 // Node has a contentEditable value 14321 if (node.nodeType === 1 && getContentEditable(node)) { 14322 lastContentEditable = contentEditable; 14323 contentEditable = getContentEditable(node) === "true"; 14324 hasContentEditableState = true; // We don't want to wrap the container only it's children 14325 } 14326 14327 // Stop wrapping on br elements 14328 if (isEq(nodeName, 'br')) { 14329 currentWrapElm = 0; 14330 14331 // Remove any br elements when we wrap things 14332 if (format.block) { 14333 dom.remove(node); 14334 } 14335 14336 return; 14337 } 14338 14339 // If node is wrapper type 14340 if (format.wrapper && matchNode(node, name, vars)) { 14341 currentWrapElm = 0; 14342 return; 14343 } 14344 14345 // Can we rename the block 14346 // TODO: Break this if up, too complex 14347 if (contentEditable && !hasContentEditableState && format.block && 14348 !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { 14349 node = dom.rename(node, wrapName); 14350 setElementFormat(node); 14351 newWrappers.push(node); 14352 currentWrapElm = 0; 14353 return; 14354 } 14355 14356 // Handle selector patterns 14357 if (format.selector) { 14358 // Look for matching formats 14359 each(formatList, function(format) { 14360 // Check collapsed state if it exists 14361 if ('collapsed' in format && format.collapsed !== isCollapsed) { 14362 return; 14363 } 14364 14365 if (dom.is(node, format.selector) && !isCaretNode(node)) { 14366 setElementFormat(node, format); 14367 found = true; 14368 } 14369 }); 14370 14371 // Continue processing if a selector match wasn't found and a inline element is defined 14372 if (!format.inline || found) { 14373 currentWrapElm = 0; 14374 return; 14375 } 14376 } 14377 14378 // Is it valid to wrap this item 14379 // TODO: Break this if up, too complex 14380 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && 14381 !(!node_specific && node.nodeType === 3 && 14382 node.nodeValue.length === 1 && 14383 node.nodeValue.charCodeAt(0) === 65279) && 14384 !isCaretNode(node) && 14385 (!format.inline || !isBlock(node))) { 14386 // Start wrapping 14387 if (!currentWrapElm) { 14388 // Wrap the node 14389 currentWrapElm = dom.clone(wrapElm, FALSE); 14390 node.parentNode.insertBefore(currentWrapElm, node); 14391 newWrappers.push(currentWrapElm); 14392 } 14393 14394 currentWrapElm.appendChild(node); 14395 } else { 14396 // Start a new wrapper for possible children 14397 currentWrapElm = 0; 14398 14399 each(grep(node.childNodes), process); 14400 14401 if (hasContentEditableState) { 14402 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 14403 } 14404 14405 // End the last wrapper 14406 currentWrapElm = 0; 14407 } 14408 } 14409 14410 // Process siblings from range 14411 each(nodes, process); 14412 }); 14413 14414 // Apply formats to links as well to get the color of the underline to change as well 14415 if (format.links === true) { 14416 each(newWrappers, function(node) { 14417 function process(node) { 14418 if (node.nodeName === 'A') { 14419 setElementFormat(node, format); 14420 } 14421 14422 each(grep(node.childNodes), process); 14423 } 14424 14425 process(node); 14426 }); 14427 } 14428 14429 // Cleanup 14430 each(newWrappers, function(node) { 14431 var childCount; 14432 14433 function getChildCount(node) { 14434 var count = 0; 14435 14436 each(node.childNodes, function(node) { 14437 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { 14438 count++; 14439 } 14440 }); 14441 14442 return count; 14443 } 14444 14445 function mergeStyles(node) { 14446 var child, clone; 14447 14448 each(node.childNodes, function(node) { 14449 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { 14450 child = node; 14451 return FALSE; // break loop 14452 } 14453 }); 14454 14455 // If child was found and of the same type as the current node 14456 if (child && !isBookmarkNode(child) && matchName(child, format)) { 14457 clone = dom.clone(child, FALSE); 14458 setElementFormat(clone); 14459 14460 dom.replace(clone, node, TRUE); 14461 dom.remove(child, 1); 14462 } 14463 14464 return clone || node; 14465 } 14466 14467 childCount = getChildCount(node); 14468 14469 // Remove empty nodes but only if there is multiple wrappers and they are not block 14470 // elements so never remove single <h1></h1> since that would remove the 14471 // currrent empty block element where the caret is at 14472 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { 14473 dom.remove(node, 1); 14474 return; 14475 } 14476 14477 if (format.inline || format.wrapper) { 14478 // Merges the current node with it's children of similar type to reduce the number of elements 14479 if (!format.exact && childCount === 1) { 14480 node = mergeStyles(node); 14481 } 14482 14483 // Remove/merge children 14484 each(formatList, function(format) { 14485 // Merge all children of similar type will move styles from child to parent 14486 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 14487 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 14488 each(dom.select(format.inline, node), function(child) { 14489 if (isBookmarkNode(child)) { 14490 return; 14491 } 14492 14493 removeFormat(format, vars, child, format.exact ? child : null); 14494 }); 14495 }); 14496 14497 // Remove child if direct parent is of same type 14498 if (matchNode(node.parentNode, name, vars)) { 14499 dom.remove(node, 1); 14500 node = 0; 14501 return TRUE; 14502 } 14503 14504 // Look for parent with similar style format 14505 if (format.merge_with_parents) { 14506 dom.getParent(node.parentNode, function(parent) { 14507 if (matchNode(parent, name, vars)) { 14508 dom.remove(node, 1); 14509 node = 0; 14510 return TRUE; 14511 } 14512 }); 14513 } 14514 14515 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 14516 if (node && format.merge_siblings !== false) { 14517 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); 14518 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); 14519 } 14520 } 14521 }); 14522 } 14523 14524 if (format) { 14525 if (node) { 14526 if (node.nodeType) { 14527 rng = dom.createRng(); 14528 rng.setStartBefore(node); 14529 rng.setEndAfter(node); 14530 applyRngStyle(expandRng(rng, formatList), null, true); 14531 } else { 14532 applyRngStyle(node, null, true); 14533 } 14534 } else { 14535 if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { 14536 // Obtain selection node before selection is unselected by applyRngStyle() 14537 var curSelNode = ed.selection.getNode(); 14538 14539 // If the formats have a default block and we can't find a parent block then 14540 // start wrapping it with a DIV this is for forced_root_blocks: false 14541 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way 14542 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { 14543 apply(formatList[0].defaultBlock); 14544 } 14545 14546 // Apply formatting to selection 14547 ed.selection.setRng(adjustSelectionToVisibleSelection()); 14548 bookmark = selection.getBookmark(); 14549 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); 14550 14551 // Colored nodes should be underlined so that the color of the underline matches the text color. 14552 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 14553 walk(curSelNode, processUnderlineAndColor, 'childNodes'); 14554 processUnderlineAndColor(curSelNode); 14555 } 14556 14557 selection.moveToBookmark(bookmark); 14558 moveStart(selection.getRng(TRUE)); 14559 ed.nodeChanged(); 14560 } else { 14561 performCaretAction('apply', name, vars); 14562 } 14563 } 14564 } 14565 } 14566 14567 /** 14568 * Removes the specified format from the current selection or specified node. 14569 * 14570 * @method remove 14571 * @param {String} name Name of format to remove. 14572 * @param {Object} vars Optional list of variables to replace within format before removing it. 14573 * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. 14574 */ 14575 function remove(name, vars, node, similar) { 14576 var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; 14577 14578 // Merges the styles for each node 14579 function process(node) { 14580 var children, i, l, lastContentEditable, hasContentEditableState; 14581 14582 // Node has a contentEditable value 14583 if (node.nodeType === 1 && getContentEditable(node)) { 14584 lastContentEditable = contentEditable; 14585 contentEditable = getContentEditable(node) === "true"; 14586 hasContentEditableState = true; // We don't want to wrap the container only it's children 14587 } 14588 14589 // Grab the children first since the nodelist might be changed 14590 children = grep(node.childNodes); 14591 14592 // Process current node 14593 if (contentEditable && !hasContentEditableState) { 14594 for (i = 0, l = formatList.length; i < l; i++) { 14595 if (removeFormat(formatList[i], vars, node, node)) { 14596 break; 14597 } 14598 } 14599 } 14600 14601 // Process the children 14602 if (format.deep) { 14603 if (children.length) { 14604 for (i = 0, l = children.length; i < l; i++) { 14605 process(children[i]); 14606 } 14607 14608 if (hasContentEditableState) { 14609 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 14610 } 14611 } 14612 } 14613 } 14614 14615 function findFormatRoot(container) { 14616 var formatRoot; 14617 14618 // Find format root 14619 each(getParents(container.parentNode).reverse(), function(parent) { 14620 var format; 14621 14622 // Find format root element 14623 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { 14624 // Is the node matching the format we are looking for 14625 format = matchNode(parent, name, vars, similar); 14626 if (format && format.split !== false) { 14627 formatRoot = parent; 14628 } 14629 } 14630 }); 14631 14632 return formatRoot; 14633 } 14634 14635 function wrapAndSplit(formatRoot, container, target, split) { 14636 var parent, clone, lastClone, firstClone, i, formatRootParent; 14637 14638 // Format root found then clone formats and split it 14639 if (formatRoot) { 14640 formatRootParent = formatRoot.parentNode; 14641 14642 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { 14643 clone = dom.clone(parent, FALSE); 14644 14645 for (i = 0; i < formatList.length; i++) { 14646 if (removeFormat(formatList[i], vars, clone, clone)) { 14647 clone = 0; 14648 break; 14649 } 14650 } 14651 14652 // Build wrapper node 14653 if (clone) { 14654 if (lastClone) { 14655 clone.appendChild(lastClone); 14656 } 14657 14658 if (!firstClone) { 14659 firstClone = clone; 14660 } 14661 14662 lastClone = clone; 14663 } 14664 } 14665 14666 // Never split block elements if the format is mixed 14667 if (split && (!format.mixed || !isBlock(formatRoot))) { 14668 container = dom.split(formatRoot, container); 14669 } 14670 14671 // Wrap container in cloned formats 14672 if (lastClone) { 14673 target.parentNode.insertBefore(lastClone, target); 14674 firstClone.appendChild(target); 14675 } 14676 } 14677 14678 return container; 14679 } 14680 14681 function splitToFormatRoot(container) { 14682 return wrapAndSplit(findFormatRoot(container), container, container, true); 14683 } 14684 14685 function unwrap(start) { 14686 var node = dom.get(start ? '_start' : '_end'), 14687 out = node[start ? 'firstChild' : 'lastChild']; 14688 14689 // If the end is placed within the start the result will be removed 14690 // So this checks if the out node is a bookmark node if it is it 14691 // checks for another more suitable node 14692 if (isBookmarkNode(out)) { 14693 out = out[start ? 'firstChild' : 'lastChild']; 14694 } 14695 14696 dom.remove(node, true); 14697 14698 return out; 14699 } 14700 14701 function removeRngStyle(rng) { 14702 var startContainer, endContainer; 14703 var commonAncestorContainer = rng.commonAncestorContainer; 14704 14705 rng = expandRng(rng, formatList, TRUE); 14706 14707 if (format.split) { 14708 startContainer = getContainer(rng, TRUE); 14709 endContainer = getContainer(rng); 14710 14711 if (startContainer != endContainer) { 14712 // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN 14713 // so let's see if we can use the first child instead 14714 // This will happen if you triple click a table cell and use remove formatting 14715 if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { 14716 if (startContainer.nodeName == "TR") { 14717 startContainer = startContainer.firstChild.firstChild || startContainer; 14718 } else { 14719 startContainer = startContainer.firstChild || startContainer; 14720 } 14721 } 14722 14723 // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 14724 if (commonAncestorContainer && 14725 /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && 14726 /^(TH|TD)$/.test(endContainer.nodeName) && endContainer.firstChild) { 14727 endContainer = endContainer.firstChild || endContainer; 14728 } 14729 14730 // Wrap start/end nodes in span element since these might be cloned/moved 14731 startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); 14732 endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'}); 14733 14734 // Split start/end 14735 splitToFormatRoot(startContainer); 14736 splitToFormatRoot(endContainer); 14737 14738 // Unwrap start/end to get real elements again 14739 startContainer = unwrap(TRUE); 14740 endContainer = unwrap(); 14741 } else { 14742 startContainer = endContainer = splitToFormatRoot(startContainer); 14743 } 14744 14745 // Update range positions since they might have changed after the split operations 14746 rng.startContainer = startContainer.parentNode; 14747 rng.startOffset = nodeIndex(startContainer); 14748 rng.endContainer = endContainer.parentNode; 14749 rng.endOffset = nodeIndex(endContainer) + 1; 14750 } 14751 14752 // Remove items between start/end 14753 rangeUtils.walk(rng, function(nodes) { 14754 each(nodes, function(node) { 14755 process(node); 14756 14757 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. 14758 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && 14759 node.parentNode && getTextDecoration(node.parentNode) === 'underline') { 14760 removeFormat({ 14761 'deep': false, 14762 'exact': true, 14763 'inline': 'span', 14764 'styles': { 14765 'textDecoration': 'underline' 14766 } 14767 }, null, node); 14768 } 14769 }); 14770 }); 14771 } 14772 14773 // Handle node 14774 if (node) { 14775 if (node.nodeType) { 14776 rng = dom.createRng(); 14777 rng.setStartBefore(node); 14778 rng.setEndAfter(node); 14779 removeRngStyle(rng); 14780 } else { 14781 removeRngStyle(node); 14782 } 14783 14784 return; 14785 } 14786 14787 if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { 14788 bookmark = selection.getBookmark(); 14789 removeRngStyle(selection.getRng(TRUE)); 14790 selection.moveToBookmark(bookmark); 14791 14792 // Check if start element still has formatting then we are at: "<b>text|</b>text" 14793 // and need to move the start into the next text node 14794 if (format.inline && match(name, vars, selection.getStart())) { 14795 moveStart(selection.getRng(true)); 14796 } 14797 14798 ed.nodeChanged(); 14799 } else { 14800 performCaretAction('remove', name, vars, similar); 14801 } 14802 } 14803 14804 /** 14805 * Toggles the specified format on/off. 14806 * 14807 * @method toggle 14808 * @param {String} name Name of format to apply/remove. 14809 * @param {Object} vars Optional list of variables to replace within format before applying/removing it. 14810 * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. 14811 */ 14812 function toggle(name, vars, node) { 14813 var fmt = get(name); 14814 14815 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { 14816 remove(name, vars, node); 14817 } else { 14818 apply(name, vars, node); 14819 } 14820 } 14821 14822 /** 14823 * Return true/false if the specified node has the specified format. 14824 * 14825 * @method matchNode 14826 * @param {Node} node Node to check the format on. 14827 * @param {String} name Format name to check. 14828 * @param {Object} vars Optional list of variables to replace before checking it. 14829 * @param {Boolean} similar Match format that has similar properties. 14830 * @return {Object} Returns the format object it matches or undefined if it doesn't match. 14831 */ 14832 function matchNode(node, name, vars, similar) { 14833 var formatList = get(name), format, i, classes; 14834 14835 function matchItems(node, format, item_name) { 14836 var key, value, items = format[item_name], i; 14837 14838 // Custom match 14839 if (format.onmatch) { 14840 return format.onmatch(node, format, item_name); 14841 } 14842 14843 // Check all items 14844 if (items) { 14845 // Non indexed object 14846 if (items.length === undef) { 14847 for (key in items) { 14848 if (items.hasOwnProperty(key)) { 14849 if (item_name === 'attributes') { 14850 value = dom.getAttrib(node, key); 14851 } else { 14852 value = getStyle(node, key); 14853 } 14854 14855 if (similar && !value && !format.exact) { 14856 return; 14857 } 14858 14859 if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { 14860 return; 14861 } 14862 } 14863 } 14864 } else { 14865 // Only one match needed for indexed arrays 14866 for (i = 0; i < items.length; i++) { 14867 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { 14868 return format; 14869 } 14870 } 14871 } 14872 } 14873 14874 return format; 14875 } 14876 14877 if (formatList && node) { 14878 // Check each format in list 14879 for (i = 0; i < formatList.length; i++) { 14880 format = formatList[i]; 14881 14882 // Name name, attributes, styles and classes 14883 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { 14884 // Match classes 14885 if ((classes = format.classes)) { 14886 for (i = 0; i < classes.length; i++) { 14887 if (!dom.hasClass(node, classes[i])) { 14888 return; 14889 } 14890 } 14891 } 14892 14893 return format; 14894 } 14895 } 14896 } 14897 } 14898 14899 /** 14900 * Matches the current selection or specified node against the specified format name. 14901 * 14902 * @method match 14903 * @param {String} name Name of format to match. 14904 * @param {Object} vars Optional list of variables to replace before checking it. 14905 * @param {Node} node Optional node to check. 14906 * @return {boolean} true/false if the specified selection/node matches the format. 14907 */ 14908 function match(name, vars, node) { 14909 var startNode; 14910 14911 function matchParents(node) { 14912 var root = dom.getRoot(); 14913 14914 if (node === root) { 14915 return false; 14916 } 14917 14918 // Find first node with similar format settings 14919 node = dom.getParent(node, function(node) { 14920 return node.parentNode === root || !!matchNode(node, name, vars, true); 14921 }); 14922 14923 // Do an exact check on the similar format element 14924 return matchNode(node, name, vars); 14925 } 14926 14927 // Check specified node 14928 if (node) { 14929 return matchParents(node); 14930 } 14931 14932 // Check selected node 14933 node = selection.getNode(); 14934 if (matchParents(node)) { 14935 return TRUE; 14936 } 14937 14938 // Check start node if it's different 14939 startNode = selection.getStart(); 14940 if (startNode != node) { 14941 if (matchParents(startNode)) { 14942 return TRUE; 14943 } 14944 } 14945 14946 return FALSE; 14947 } 14948 14949 /** 14950 * Matches the current selection against the array of formats and returns a new array with matching formats. 14951 * 14952 * @method matchAll 14953 * @param {Array} names Name of format to match. 14954 * @param {Object} vars Optional list of variables to replace before checking it. 14955 * @return {Array} Array with matched formats. 14956 */ 14957 function matchAll(names, vars) { 14958 var startElement, matchedFormatNames = [], checkedMap = {}; 14959 14960 // Check start of selection for formats 14961 startElement = selection.getStart(); 14962 dom.getParent(startElement, function(node) { 14963 var i, name; 14964 14965 for (i = 0; i < names.length; i++) { 14966 name = names[i]; 14967 14968 if (!checkedMap[name] && matchNode(node, name, vars)) { 14969 checkedMap[name] = true; 14970 matchedFormatNames.push(name); 14971 } 14972 } 14973 }, dom.getRoot()); 14974 14975 return matchedFormatNames; 14976 } 14977 14978 /** 14979 * Returns true/false if the specified format can be applied to the current selection or not. It 14980 * will currently only check the state for selector formats, it returns true on all other format types. 14981 * 14982 * @method canApply 14983 * @param {String} name Name of format to check. 14984 * @return {boolean} true/false if the specified format can be applied to the current selection/node. 14985 */ 14986 function canApply(name) { 14987 var formatList = get(name), startNode, parents, i, x, selector; 14988 14989 if (formatList) { 14990 startNode = selection.getStart(); 14991 parents = getParents(startNode); 14992 14993 for (x = formatList.length - 1; x >= 0; x--) { 14994 selector = formatList[x].selector; 14995 14996 // Format is not selector based then always return TRUE 14997 // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line 14998 if (!selector || formatList[x].defaultBlock) { 14999 return TRUE; 15000 } 15001 15002 for (i = parents.length - 1; i >= 0; i--) { 15003 if (dom.is(parents[i], selector)) { 15004 return TRUE; 15005 } 15006 } 15007 } 15008 } 15009 15010 return FALSE; 15011 } 15012 15013 /** 15014 * Executes the specified callback when the current selection matches the formats or not. 15015 * 15016 * @method formatChanged 15017 * @param {String} formats Comma separated list of formats to check for. 15018 * @param {function} callback Callback with state and args when the format is changed/toggled on/off. 15019 * @param {Boolean} similar True/false state if the match should handle similar or exact formats. 15020 */ 15021 function formatChanged(formats, callback, similar) { 15022 var currentFormats; 15023 15024 // Setup format node change logic 15025 if (!formatChangeData) { 15026 formatChangeData = {}; 15027 currentFormats = {}; 15028 15029 ed.on('NodeChange', function(e) { 15030 var parents = getParents(e.element), matchedFormats = {}; 15031 15032 // Ignore bogus nodes like the <a> tag created by moveStart() 15033 parents = Tools.grep(parents, function(node) { 15034 return node.nodeType == 1 && !node.getAttribute('data-mce-bogus'); 15035 }); 15036 15037 // Check for new formats 15038 each(formatChangeData, function(callbacks, format) { 15039 each(parents, function(node) { 15040 if (matchNode(node, format, {}, callbacks.similar)) { 15041 if (!currentFormats[format]) { 15042 // Execute callbacks 15043 each(callbacks, function(callback) { 15044 callback(true, {node: node, format: format, parents: parents}); 15045 }); 15046 15047 currentFormats[format] = callbacks; 15048 } 15049 15050 matchedFormats[format] = callbacks; 15051 return false; 15052 } 15053 }); 15054 }); 15055 15056 // Check if current formats still match 15057 each(currentFormats, function(callbacks, format) { 15058 if (!matchedFormats[format]) { 15059 delete currentFormats[format]; 15060 15061 each(callbacks, function(callback) { 15062 callback(false, {node: e.element, format: format, parents: parents}); 15063 }); 15064 } 15065 }); 15066 }); 15067 } 15068 15069 // Add format listeners 15070 each(formats.split(','), function(format) { 15071 if (!formatChangeData[format]) { 15072 formatChangeData[format] = []; 15073 formatChangeData[format].similar = similar; 15074 } 15075 15076 formatChangeData[format].push(callback); 15077 }); 15078 15079 return this; 15080 } 15081 15082 /** 15083 * Returns a preview css text for the specified format. 15084 * 15085 * @method getCssText 15086 * @param {String/Object} format Format to generate preview css text for. 15087 * @return {String} Css text for the specified format. 15088 * @example 15089 * var cssText1 = editor.formatter.getCssText('bold'); 15090 * var cssText2 = editor.formatter.getCssText({inline: 'b'}); 15091 */ 15092 function getCssText(format) { 15093 return Preview.getCssText(ed, format); 15094 } 15095 15096 // Expose to public 15097 extend(this, { 15098 get: get, 15099 register: register, 15100 unregister: unregister, 15101 apply: apply, 15102 remove: remove, 15103 toggle: toggle, 15104 match: match, 15105 matchAll: matchAll, 15106 matchNode: matchNode, 15107 canApply: canApply, 15108 formatChanged: formatChanged, 15109 getCssText: getCssText 15110 }); 15111 15112 // Initialize 15113 defaultFormats(); 15114 addKeyboardShortcuts(); 15115 ed.on('BeforeGetContent', function(e) { 15116 if (markCaretContainersBogus && e.format != 'raw') { 15117 markCaretContainersBogus(); 15118 } 15119 }); 15120 ed.on('mouseup keydown', function(e) { 15121 if (disableCaretContainer) { 15122 disableCaretContainer(e); 15123 } 15124 }); 15125 15126 // Private functions 15127 15128 /** 15129 * Checks if the specified nodes name matches the format inline/block or selector. 15130 * 15131 * @private 15132 * @param {Node} node Node to match against the specified format. 15133 * @param {Object} format Format object o match with. 15134 * @return {boolean} true/false if the format matches. 15135 */ 15136 function matchName(node, format) { 15137 // Check for inline match 15138 if (isEq(node, format.inline)) { 15139 return TRUE; 15140 } 15141 15142 // Check for block match 15143 if (isEq(node, format.block)) { 15144 return TRUE; 15145 } 15146 15147 // Check for selector match 15148 if (format.selector) { 15149 return node.nodeType == 1 && dom.is(node, format.selector); 15150 } 15151 } 15152 15153 /** 15154 * Compares two string/nodes regardless of their case. 15155 * 15156 * @private 15157 * @param {String/Node} Node or string to compare. 15158 * @param {String/Node} Node or string to compare. 15159 * @return {boolean} True/false if they match. 15160 */ 15161 function isEq(str1, str2) { 15162 str1 = str1 || ''; 15163 str2 = str2 || ''; 15164 15165 str1 = '' + (str1.nodeName || str1); 15166 str2 = '' + (str2.nodeName || str2); 15167 15168 return str1.toLowerCase() == str2.toLowerCase(); 15169 } 15170 15171 /** 15172 * Returns the style by name on the specified node. This method modifies the style 15173 * contents to make it more easy to match. This will resolve a few browser issues. 15174 * 15175 * @private 15176 * @param {Node} node to get style from. 15177 * @param {String} name Style name to get. 15178 * @return {String} Style item value. 15179 */ 15180 function getStyle(node, name) { 15181 return normalizeStyleValue(dom.getStyle(node, name), name); 15182 } 15183 15184 /** 15185 * Normalize style value by name. This method modifies the style contents 15186 * to make it more easy to match. This will resolve a few browser issues. 15187 * 15188 * @private 15189 * @param {Node} node to get style from. 15190 * @param {String} name Style name to get. 15191 * @return {String} Style item value. 15192 */ 15193 function normalizeStyleValue(value, name) { 15194 // Force the format to hex 15195 if (name == 'color' || name == 'backgroundColor') { 15196 value = dom.toHex(value); 15197 } 15198 15199 // Opera will return bold as 700 15200 if (name == 'fontWeight' && value == 700) { 15201 value = 'bold'; 15202 } 15203 15204 // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" 15205 if (name == 'fontFamily') { 15206 value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); 15207 } 15208 15209 return '' + value; 15210 } 15211 15212 /** 15213 * Replaces variables in the value. The variable format is %var. 15214 * 15215 * @private 15216 * @param {String} value Value to replace variables in. 15217 * @param {Object} vars Name/value array with variables to replace. 15218 * @return {String} New value with replaced variables. 15219 */ 15220 function replaceVars(value, vars) { 15221 if (typeof(value) != "string") { 15222 value = value(vars); 15223 } else if (vars) { 15224 value = value.replace(/%(\w+)/g, function(str, name) { 15225 return vars[name] || str; 15226 }); 15227 } 15228 15229 return value; 15230 } 15231 15232 function isWhiteSpaceNode(node) { 15233 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); 15234 } 15235 15236 function wrap(node, name, attrs) { 15237 var wrapper = dom.create(name, attrs); 15238 15239 node.parentNode.insertBefore(wrapper, node); 15240 wrapper.appendChild(node); 15241 15242 return wrapper; 15243 } 15244 15245 /** 15246 * Expands the specified range like object to depending on format. 15247 * 15248 * For example on block formats it will move the start/end position 15249 * to the beginning of the current block. 15250 * 15251 * @private 15252 * @param {Object} rng Range like object. 15253 * @param {Array} formats Array with formats to expand by. 15254 * @return {Object} Expanded range like object. 15255 */ 15256 function expandRng(rng, format, remove) { 15257 var lastIdx, leaf, endPoint, 15258 startContainer = rng.startContainer, 15259 startOffset = rng.startOffset, 15260 endContainer = rng.endContainer, 15261 endOffset = rng.endOffset; 15262 15263 // This function walks up the tree if there is no siblings before/after the node 15264 function findParentContainer(start) { 15265 var container, parent, sibling, siblingName, root; 15266 15267 container = parent = start ? startContainer : endContainer; 15268 siblingName = start ? 'previousSibling' : 'nextSibling'; 15269 root = dom.getRoot(); 15270 15271 function isBogusBr(node) { 15272 return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; 15273 } 15274 15275 // If it's a text node and the offset is inside the text 15276 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { 15277 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { 15278 return container; 15279 } 15280 } 15281 15282 /*eslint no-constant-condition:0 */ 15283 while (true) { 15284 // Stop expanding on block elements 15285 if (!format[0].block_expand && isBlock(parent)) { 15286 return parent; 15287 } 15288 15289 // Walk left/right 15290 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 15291 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { 15292 return parent; 15293 } 15294 } 15295 15296 // Check if we can move up are we at root level or body level 15297 if (parent.parentNode == root) { 15298 container = parent; 15299 break; 15300 } 15301 15302 parent = parent.parentNode; 15303 } 15304 15305 return container; 15306 } 15307 15308 // This function walks down the tree to find the leaf at the selection. 15309 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. 15310 function findLeaf(node, offset) { 15311 if (offset === undef) { 15312 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15313 } 15314 15315 while (node && node.hasChildNodes()) { 15316 node = node.childNodes[offset]; 15317 if (node) { 15318 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15319 } 15320 } 15321 return {node: node, offset: offset}; 15322 } 15323 15324 // If index based start position then resolve it 15325 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 15326 lastIdx = startContainer.childNodes.length - 1; 15327 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; 15328 15329 if (startContainer.nodeType == 3) { 15330 startOffset = 0; 15331 } 15332 } 15333 15334 // If index based end position then resolve it 15335 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 15336 lastIdx = endContainer.childNodes.length - 1; 15337 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; 15338 15339 if (endContainer.nodeType == 3) { 15340 endOffset = endContainer.nodeValue.length; 15341 } 15342 } 15343 15344 // Expands the node to the closes contentEditable false element if it exists 15345 function findParentContentEditable(node) { 15346 var parent = node; 15347 15348 while (parent) { 15349 if (parent.nodeType === 1 && getContentEditable(parent)) { 15350 return getContentEditable(parent) === "false" ? parent : node; 15351 } 15352 15353 parent = parent.parentNode; 15354 } 15355 15356 return node; 15357 } 15358 15359 function findWordEndPoint(container, offset, start) { 15360 var walker, node, pos, lastTextNode; 15361 15362 function findSpace(node, offset) { 15363 var pos, pos2, str = node.nodeValue; 15364 15365 if (typeof(offset) == "undefined") { 15366 offset = start ? str.length : 0; 15367 } 15368 15369 if (start) { 15370 pos = str.lastIndexOf(' ', offset); 15371 pos2 = str.lastIndexOf('\u00a0', offset); 15372 pos = pos > pos2 ? pos : pos2; 15373 15374 // Include the space on remove to avoid tag soup 15375 if (pos !== -1 && !remove) { 15376 pos++; 15377 } 15378 } else { 15379 pos = str.indexOf(' ', offset); 15380 pos2 = str.indexOf('\u00a0', offset); 15381 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; 15382 } 15383 15384 return pos; 15385 } 15386 15387 if (container.nodeType === 3) { 15388 pos = findSpace(container, offset); 15389 15390 if (pos !== -1) { 15391 return {container: container, offset: pos}; 15392 } 15393 15394 lastTextNode = container; 15395 } 15396 15397 // Walk the nodes inside the block 15398 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); 15399 while ((node = walker[start ? 'prev' : 'next']())) { 15400 if (node.nodeType === 3) { 15401 lastTextNode = node; 15402 pos = findSpace(node); 15403 15404 if (pos !== -1) { 15405 return {container: node, offset: pos}; 15406 } 15407 } else if (isBlock(node)) { 15408 break; 15409 } 15410 } 15411 15412 if (lastTextNode) { 15413 if (start) { 15414 offset = 0; 15415 } else { 15416 offset = lastTextNode.length; 15417 } 15418 15419 return {container: lastTextNode, offset: offset}; 15420 } 15421 } 15422 15423 function findSelectorEndPoint(container, sibling_name) { 15424 var parents, i, y, curFormat; 15425 15426 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) { 15427 container = container[sibling_name]; 15428 } 15429 15430 parents = getParents(container); 15431 for (i = 0; i < parents.length; i++) { 15432 for (y = 0; y < format.length; y++) { 15433 curFormat = format[y]; 15434 15435 // If collapsed state is set then skip formats that doesn't match that 15436 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { 15437 continue; 15438 } 15439 15440 if (dom.is(parents[i], curFormat.selector)) { 15441 return parents[i]; 15442 } 15443 } 15444 } 15445 15446 return container; 15447 } 15448 15449 function findBlockEndPoint(container, sibling_name) { 15450 var node, root = dom.getRoot(); 15451 15452 // Expand to block of similar type 15453 if (!format[0].wrapper) { 15454 node = dom.getParent(container, format[0].block, root); 15455 } 15456 15457 // Expand to first wrappable block element or any block element 15458 if (!node) { 15459 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) { 15460 // Fixes #6183 where it would expand to editable parent element in inline mode 15461 return node != root && isTextBlock(node); 15462 }); 15463 } 15464 15465 // Exclude inner lists from wrapping 15466 if (node && format[0].wrapper) { 15467 node = getParents(node, 'ul,ol').reverse()[0] || node; 15468 } 15469 15470 // Didn't find a block element look for first/last wrappable element 15471 if (!node) { 15472 node = container; 15473 15474 while (node[sibling_name] && !isBlock(node[sibling_name])) { 15475 node = node[sibling_name]; 15476 15477 // Break on BR but include it will be removed later on 15478 // we can't remove it now since we need to check if it can be wrapped 15479 if (isEq(node, 'br')) { 15480 break; 15481 } 15482 } 15483 } 15484 15485 return node || container; 15486 } 15487 15488 // Expand to closest contentEditable element 15489 startContainer = findParentContentEditable(startContainer); 15490 endContainer = findParentContentEditable(endContainer); 15491 15492 // Exclude bookmark nodes if possible 15493 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { 15494 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 15495 startContainer = startContainer.nextSibling || startContainer; 15496 15497 if (startContainer.nodeType == 3) { 15498 startOffset = 0; 15499 } 15500 } 15501 15502 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { 15503 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 15504 endContainer = endContainer.previousSibling || endContainer; 15505 15506 if (endContainer.nodeType == 3) { 15507 endOffset = endContainer.length; 15508 } 15509 } 15510 15511 if (format[0].inline) { 15512 if (rng.collapsed) { 15513 // Expand left to closest word boundary 15514 endPoint = findWordEndPoint(startContainer, startOffset, true); 15515 if (endPoint) { 15516 startContainer = endPoint.container; 15517 startOffset = endPoint.offset; 15518 } 15519 15520 // Expand right to closest word boundary 15521 endPoint = findWordEndPoint(endContainer, endOffset); 15522 if (endPoint) { 15523 endContainer = endPoint.container; 15524 endOffset = endPoint.offset; 15525 } 15526 } 15527 15528 // Avoid applying formatting to a trailing space. 15529 leaf = findLeaf(endContainer, endOffset); 15530 if (leaf.node) { 15531 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { 15532 leaf = findLeaf(leaf.node.previousSibling); 15533 } 15534 15535 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && 15536 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { 15537 15538 if (leaf.offset > 1) { 15539 endContainer = leaf.node; 15540 endContainer.splitText(leaf.offset - 1); 15541 } 15542 } 15543 } 15544 } 15545 15546 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 15547 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 15548 // This will reduce the number of wrapper elements that needs to be created 15549 // Move start point up the tree 15550 if (format[0].inline || format[0].block_expand) { 15551 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { 15552 startContainer = findParentContainer(true); 15553 } 15554 15555 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { 15556 endContainer = findParentContainer(); 15557 } 15558 } 15559 15560 // Expand start/end container to matching selector 15561 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { 15562 // Find new startContainer/endContainer if there is better one 15563 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); 15564 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); 15565 } 15566 15567 // Expand start/end container to matching block element or text node 15568 if (format[0].block || format[0].selector) { 15569 // Find new startContainer/endContainer if there is better one 15570 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); 15571 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); 15572 15573 // Non block element then try to expand up the leaf 15574 if (format[0].block) { 15575 if (!isBlock(startContainer)) { 15576 startContainer = findParentContainer(true); 15577 } 15578 15579 if (!isBlock(endContainer)) { 15580 endContainer = findParentContainer(); 15581 } 15582 } 15583 } 15584 15585 // Setup index for startContainer 15586 if (startContainer.nodeType == 1) { 15587 startOffset = nodeIndex(startContainer); 15588 startContainer = startContainer.parentNode; 15589 } 15590 15591 // Setup index for endContainer 15592 if (endContainer.nodeType == 1) { 15593 endOffset = nodeIndex(endContainer) + 1; 15594 endContainer = endContainer.parentNode; 15595 } 15596 15597 // Return new range like object 15598 return { 15599 startContainer: startContainer, 15600 startOffset: startOffset, 15601 endContainer: endContainer, 15602 endOffset: endOffset 15603 }; 15604 } 15605 15606 function isColorFormatAndAnchor(node, format) { 15607 return format.links && node.tagName == 'A'; 15608 } 15609 15610 /** 15611 * Removes the specified format for the specified node. It will also remove the node if it doesn't have 15612 * any attributes if the format specifies it to do so. 15613 * 15614 * @private 15615 * @param {Object} format Format object with items to remove from node. 15616 * @param {Object} vars Name/value object with variables to apply to format. 15617 * @param {Node} node Node to remove the format styles on. 15618 * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. 15619 * @return {Boolean} True/false if the node was removed or not. 15620 */ 15621 function removeFormat(format, vars, node, compare_node) { 15622 var i, attrs, stylesModified; 15623 15624 // Check if node matches format 15625 if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { 15626 return FALSE; 15627 } 15628 15629 // Should we compare with format attribs and styles 15630 if (format.remove != 'all') { 15631 // Remove styles 15632 each(format.styles, function(value, name) { 15633 value = normalizeStyleValue(replaceVars(value, vars), name); 15634 15635 // Indexed array 15636 if (typeof(name) === 'number') { 15637 name = value; 15638 compare_node = 0; 15639 } 15640 15641 if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) { 15642 dom.setStyle(node, name, ''); 15643 } 15644 15645 stylesModified = 1; 15646 }); 15647 15648 // Remove style attribute if it's empty 15649 if (stylesModified && dom.getAttrib(node, 'style') === '') { 15650 node.removeAttribute('style'); 15651 node.removeAttribute('data-mce-style'); 15652 } 15653 15654 // Remove attributes 15655 each(format.attributes, function(value, name) { 15656 var valueOut; 15657 15658 value = replaceVars(value, vars); 15659 15660 // Indexed array 15661 if (typeof(name) === 'number') { 15662 name = value; 15663 compare_node = 0; 15664 } 15665 15666 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { 15667 // Keep internal classes 15668 if (name == 'class') { 15669 value = dom.getAttrib(node, name); 15670 if (value) { 15671 // Build new class value where everything is removed except the internal prefixed classes 15672 valueOut = ''; 15673 each(value.split(/\s+/), function(cls) { 15674 if (/mce\w+/.test(cls)) { 15675 valueOut += (valueOut ? ' ' : '') + cls; 15676 } 15677 }); 15678 15679 // We got some internal classes left 15680 if (valueOut) { 15681 dom.setAttrib(node, name, valueOut); 15682 return; 15683 } 15684 } 15685 } 15686 15687 // IE6 has a bug where the attribute doesn't get removed correctly 15688 if (name == "class") { 15689 node.removeAttribute('className'); 15690 } 15691 15692 // Remove mce prefixed attributes 15693 if (MCE_ATTR_RE.test(name)) { 15694 node.removeAttribute('data-mce-' + name); 15695 } 15696 15697 node.removeAttribute(name); 15698 } 15699 }); 15700 15701 // Remove classes 15702 each(format.classes, function(value) { 15703 value = replaceVars(value, vars); 15704 15705 if (!compare_node || dom.hasClass(compare_node, value)) { 15706 dom.removeClass(node, value); 15707 } 15708 }); 15709 15710 // Check for non internal attributes 15711 attrs = dom.getAttribs(node); 15712 for (i = 0; i < attrs.length; i++) { 15713 if (attrs[i].nodeName.indexOf('_') !== 0) { 15714 return FALSE; 15715 } 15716 } 15717 } 15718 15719 // Remove the inline child if it's empty for example <b> or <span> 15720 if (format.remove != 'none') { 15721 removeNode(node, format); 15722 return TRUE; 15723 } 15724 } 15725 15726 /** 15727 * Removes the node and wrap it's children in paragraphs before doing so or 15728 * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. 15729 * 15730 * If the div in the node below gets removed: 15731 * text<div>text</div>text 15732 * 15733 * Output becomes: 15734 * text<div><br />text<br /></div>text 15735 * 15736 * So when the div is removed the result is: 15737 * text<br />text<br />text 15738 * 15739 * @private 15740 * @param {Node} node Node to remove + apply BR/P elements to. 15741 * @param {Object} format Format rule. 15742 * @return {Node} Input node. 15743 */ 15744 function removeNode(node, format) { 15745 var parentNode = node.parentNode, rootBlockElm; 15746 15747 function find(node, next, inc) { 15748 node = getNonWhiteSpaceSibling(node, next, inc); 15749 15750 return !node || (node.nodeName == 'BR' || isBlock(node)); 15751 } 15752 15753 if (format.block) { 15754 if (!forcedRootBlock) { 15755 // Append BR elements if needed before we remove the block 15756 if (isBlock(node) && !isBlock(parentNode)) { 15757 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) { 15758 node.insertBefore(dom.create('br'), node.firstChild); 15759 } 15760 15761 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) { 15762 node.appendChild(dom.create('br')); 15763 } 15764 } 15765 } else { 15766 // Wrap the block in a forcedRootBlock if we are at the root of document 15767 if (parentNode == dom.getRoot()) { 15768 if (!format.list_block || !isEq(node, format.list_block)) { 15769 each(grep(node.childNodes), function(node) { 15770 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { 15771 if (!rootBlockElm) { 15772 rootBlockElm = wrap(node, forcedRootBlock); 15773 dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs); 15774 } else { 15775 rootBlockElm.appendChild(node); 15776 } 15777 } else { 15778 rootBlockElm = 0; 15779 } 15780 }); 15781 } 15782 } 15783 } 15784 } 15785 15786 // Never remove nodes that isn't the specified inline element if a selector is specified too 15787 if (format.selector && format.inline && !isEq(format.inline, node)) { 15788 return; 15789 } 15790 15791 dom.remove(node, 1); 15792 } 15793 15794 /** 15795 * Returns the next/previous non whitespace node. 15796 * 15797 * @private 15798 * @param {Node} node Node to start at. 15799 * @param {boolean} next (Optional) Include next or previous node defaults to previous. 15800 * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false. 15801 * @return {Node} Next or previous node or undefined if it wasn't found. 15802 */ 15803 function getNonWhiteSpaceSibling(node, next, inc) { 15804 if (node) { 15805 next = next ? 'nextSibling' : 'previousSibling'; 15806 15807 for (node = inc ? node : node[next]; node; node = node[next]) { 15808 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) { 15809 return node; 15810 } 15811 } 15812 } 15813 } 15814 15815 /** 15816 * Merges the next/previous sibling element if they match. 15817 * 15818 * @private 15819 * @param {Node} prev Previous node to compare/merge. 15820 * @param {Node} next Next node to compare/merge. 15821 * @return {Node} Next node if we didn't merge and prev node if we did. 15822 */ 15823 function mergeSiblings(prev, next) { 15824 var sibling, tmpSibling, elementUtils = new ElementUtils(dom); 15825 15826 function findElementSibling(node, sibling_name) { 15827 for (sibling = node; sibling; sibling = sibling[sibling_name]) { 15828 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) { 15829 return node; 15830 } 15831 15832 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) { 15833 return sibling; 15834 } 15835 } 15836 15837 return node; 15838 } 15839 15840 // Check if next/prev exists and that they are elements 15841 if (prev && next) { 15842 // If previous sibling is empty then jump over it 15843 prev = findElementSibling(prev, 'previousSibling'); 15844 next = findElementSibling(next, 'nextSibling'); 15845 15846 // Compare next and previous nodes 15847 if (elementUtils.compare(prev, next)) { 15848 // Append nodes between 15849 for (sibling = prev.nextSibling; sibling && sibling != next;) { 15850 tmpSibling = sibling; 15851 sibling = sibling.nextSibling; 15852 prev.appendChild(tmpSibling); 15853 } 15854 15855 // Remove next node 15856 dom.remove(next); 15857 15858 // Move children into prev node 15859 each(grep(next.childNodes), function(node) { 15860 prev.appendChild(node); 15861 }); 15862 15863 return prev; 15864 } 15865 } 15866 15867 return next; 15868 } 15869 15870 function getContainer(rng, start) { 15871 var container, offset, lastIdx; 15872 15873 container = rng[start ? 'startContainer' : 'endContainer']; 15874 offset = rng[start ? 'startOffset' : 'endOffset']; 15875 15876 if (container.nodeType == 1) { 15877 lastIdx = container.childNodes.length - 1; 15878 15879 if (!start && offset) { 15880 offset--; 15881 } 15882 15883 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 15884 } 15885 15886 // If start text node is excluded then walk to the next node 15887 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { 15888 container = new TreeWalker(container, ed.getBody()).next() || container; 15889 } 15890 15891 // If end text node is excluded then walk to the previous node 15892 if (container.nodeType === 3 && !start && offset === 0) { 15893 container = new TreeWalker(container, ed.getBody()).prev() || container; 15894 } 15895 15896 return container; 15897 } 15898 15899 function performCaretAction(type, name, vars, similar) { 15900 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; 15901 15902 // Creates a caret container bogus element 15903 function createCaretContainer(fill) { 15904 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); 15905 15906 if (fill) { 15907 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); 15908 } 15909 15910 return caretContainer; 15911 } 15912 15913 function isCaretContainerEmpty(node, nodes) { 15914 while (node) { 15915 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { 15916 return false; 15917 } 15918 15919 // Collect nodes 15920 if (nodes && node.nodeType === 1) { 15921 nodes.push(node); 15922 } 15923 15924 node = node.firstChild; 15925 } 15926 15927 return true; 15928 } 15929 15930 // Returns any parent caret container element 15931 function getParentCaretContainer(node) { 15932 while (node) { 15933 if (node.id === caretContainerId) { 15934 return node; 15935 } 15936 15937 node = node.parentNode; 15938 } 15939 } 15940 15941 // Finds the first text node in the specified node 15942 function findFirstTextNode(node) { 15943 var walker; 15944 15945 if (node) { 15946 walker = new TreeWalker(node, node); 15947 15948 for (node = walker.current(); node; node = walker.next()) { 15949 if (node.nodeType === 3) { 15950 return node; 15951 } 15952 } 15953 } 15954 } 15955 15956 // Removes the caret container for the specified node or all on the current document 15957 function removeCaretContainer(node, move_caret) { 15958 var child, rng; 15959 15960 if (!node) { 15961 node = getParentCaretContainer(selection.getStart()); 15962 15963 if (!node) { 15964 while ((node = dom.get(caretContainerId))) { 15965 removeCaretContainer(node, false); 15966 } 15967 } 15968 } else { 15969 rng = selection.getRng(true); 15970 15971 if (isCaretContainerEmpty(node)) { 15972 if (move_caret !== false) { 15973 rng.setStartBefore(node); 15974 rng.setEndBefore(node); 15975 } 15976 15977 dom.remove(node); 15978 } else { 15979 child = findFirstTextNode(node); 15980 15981 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { 15982 child.deleteData(0, 1); 15983 15984 // Fix for bug #6976 15985 if (rng.startContainer == child && rng.startOffset > 0) { 15986 rng.setStart(child, rng.startOffset - 1); 15987 } 15988 15989 if (rng.endContainer == child && rng.endOffset > 0) { 15990 rng.setEnd(child, rng.endOffset - 1); 15991 } 15992 } 15993 15994 dom.remove(node, 1); 15995 } 15996 15997 selection.setRng(rng); 15998 } 15999 } 16000 16001 // Applies formatting to the caret postion 16002 function applyCaretFormat() { 16003 var rng, caretContainer, textNode, offset, bookmark, container, text; 16004 16005 rng = selection.getRng(true); 16006 offset = rng.startOffset; 16007 container = rng.startContainer; 16008 text = container.nodeValue; 16009 16010 caretContainer = getParentCaretContainer(selection.getStart()); 16011 if (caretContainer) { 16012 textNode = findFirstTextNode(caretContainer); 16013 } 16014 16015 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character 16016 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { 16017 // Get bookmark of caret position 16018 bookmark = selection.getBookmark(); 16019 16020 // Collapse bookmark range (WebKit) 16021 rng.collapse(true); 16022 16023 // Expand the range to the closest word and split it at those points 16024 rng = expandRng(rng, get(name)); 16025 rng = rangeUtils.split(rng); 16026 16027 // Apply the format to the range 16028 apply(name, vars, rng); 16029 16030 // Move selection back to caret position 16031 selection.moveToBookmark(bookmark); 16032 } else { 16033 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { 16034 caretContainer = createCaretContainer(true); 16035 textNode = caretContainer.firstChild; 16036 16037 rng.insertNode(caretContainer); 16038 offset = 1; 16039 16040 apply(name, vars, caretContainer); 16041 } else { 16042 apply(name, vars, caretContainer); 16043 } 16044 16045 // Move selection to text node 16046 selection.setCursorLocation(textNode, offset); 16047 } 16048 } 16049 16050 function removeCaretFormat() { 16051 var rng = selection.getRng(true), container, offset, bookmark, 16052 hasContentAfter, node, formatNode, parents = [], i, caretContainer; 16053 16054 container = rng.startContainer; 16055 offset = rng.startOffset; 16056 node = container; 16057 16058 if (container.nodeType == 3) { 16059 if (offset != container.nodeValue.length) { 16060 hasContentAfter = true; 16061 } 16062 16063 node = node.parentNode; 16064 } 16065 16066 while (node) { 16067 if (matchNode(node, name, vars, similar)) { 16068 formatNode = node; 16069 break; 16070 } 16071 16072 if (node.nextSibling) { 16073 hasContentAfter = true; 16074 } 16075 16076 parents.push(node); 16077 node = node.parentNode; 16078 } 16079 16080 // Node doesn't have the specified format 16081 if (!formatNode) { 16082 return; 16083 } 16084 16085 // Is there contents after the caret then remove the format on the element 16086 if (hasContentAfter) { 16087 // Get bookmark of caret position 16088 bookmark = selection.getBookmark(); 16089 16090 // Collapse bookmark range (WebKit) 16091 rng.collapse(true); 16092 16093 // Expand the range to the closest word and split it at those points 16094 rng = expandRng(rng, get(name), true); 16095 rng = rangeUtils.split(rng); 16096 16097 // Remove the format from the range 16098 remove(name, vars, rng); 16099 16100 // Move selection back to caret position 16101 selection.moveToBookmark(bookmark); 16102 } else { 16103 caretContainer = createCaretContainer(); 16104 16105 node = caretContainer; 16106 for (i = parents.length - 1; i >= 0; i--) { 16107 node.appendChild(dom.clone(parents[i], false)); 16108 node = node.firstChild; 16109 } 16110 16111 // Insert invisible character into inner most format element 16112 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); 16113 node = node.firstChild; 16114 16115 var block = dom.getParent(formatNode, isTextBlock); 16116 16117 if (block && dom.isEmpty(block)) { 16118 // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p> 16119 formatNode.parentNode.replaceChild(caretContainer, formatNode); 16120 } else { 16121 // Insert caret container after the formated node 16122 dom.insertAfter(caretContainer, formatNode); 16123 } 16124 16125 // Move selection to text node 16126 selection.setCursorLocation(node, 1); 16127 16128 // If the formatNode is empty, we can remove it safely. 16129 if (dom.isEmpty(formatNode)) { 16130 dom.remove(formatNode); 16131 } 16132 } 16133 } 16134 16135 // Checks if the parent caret container node isn't empty if that is the case it 16136 // will remove the bogus state on all children that isn't empty 16137 function unmarkBogusCaretParents() { 16138 var caretContainer; 16139 16140 caretContainer = getParentCaretContainer(selection.getStart()); 16141 if (caretContainer && !dom.isEmpty(caretContainer)) { 16142 walk(caretContainer, function(node) { 16143 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { 16144 dom.setAttrib(node, 'data-mce-bogus', null); 16145 } 16146 }, 'childNodes'); 16147 } 16148 } 16149 16150 // Only bind the caret events once 16151 if (!ed._hasCaretEvents) { 16152 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements 16153 markCaretContainersBogus = function() { 16154 var nodes = [], i; 16155 16156 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { 16157 // Mark children 16158 i = nodes.length; 16159 while (i--) { 16160 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); 16161 } 16162 } 16163 }; 16164 16165 disableCaretContainer = function(e) { 16166 var keyCode = e.keyCode; 16167 16168 removeCaretContainer(); 16169 16170 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys 16171 if (keyCode == 8 || keyCode == 37 || keyCode == 39) { 16172 removeCaretContainer(getParentCaretContainer(selection.getStart())); 16173 } 16174 16175 unmarkBogusCaretParents(); 16176 }; 16177 16178 // Remove bogus state if they got filled by contents using editor.selection.setContent 16179 ed.on('SetContent', function(e) { 16180 if (e.selection) { 16181 unmarkBogusCaretParents(); 16182 } 16183 }); 16184 ed._hasCaretEvents = true; 16185 } 16186 16187 // Do apply or remove caret format 16188 if (type == "apply") { 16189 applyCaretFormat(); 16190 } else { 16191 removeCaretFormat(); 16192 } 16193 } 16194 16195 /** 16196 * Moves the start to the first suitable text node. 16197 */ 16198 function moveStart(rng) { 16199 var container = rng.startContainer, 16200 offset = rng.startOffset, isAtEndOfText, 16201 walker, node, nodes, tmpNode; 16202 16203 // Convert text node into index if possible 16204 if (container.nodeType == 3 && offset >= container.nodeValue.length) { 16205 // Get the parent container location and walk from there 16206 offset = nodeIndex(container); 16207 container = container.parentNode; 16208 isAtEndOfText = true; 16209 } 16210 16211 // Move startContainer/startOffset in to a suitable node 16212 if (container.nodeType == 1) { 16213 nodes = container.childNodes; 16214 container = nodes[Math.min(offset, nodes.length - 1)]; 16215 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); 16216 16217 // If offset is at end of the parent node walk to the next one 16218 if (offset > nodes.length - 1 || isAtEndOfText) { 16219 walker.next(); 16220 } 16221 16222 for (node = walker.current(); node; node = walker.next()) { 16223 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { 16224 // IE has a "neat" feature where it moves the start node into the closest element 16225 // we can avoid this by inserting an element before it and then remove it after we set the selection 16226 tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR); 16227 node.parentNode.insertBefore(tmpNode, node); 16228 16229 // Set selection and remove tmpNode 16230 rng.setStart(node, 0); 16231 selection.setRng(rng); 16232 dom.remove(tmpNode); 16233 16234 return; 16235 } 16236 } 16237 } 16238 } 16239 }; 16240 }); 16241 16242 // Included from: js/tinymce/classes/UndoManager.js 16243 16244 /** 16245 * UndoManager.js 16246 * 16247 * Copyright, Moxiecode Systems AB 16248 * Released under LGPL License. 16249 * 16250 * License: http://www.tinymce.com/license 16251 * Contributing: http://www.tinymce.com/contributing 16252 */ 16253 16254 /** 16255 * This class handles the undo/redo history levels for the editor. Since the build in undo/redo has major drawbacks a custom one was needed. 16256 * 16257 * @class tinymce.UndoManager 16258 */ 16259 define("tinymce/UndoManager", [ 16260 "tinymce/Env", 16261 "tinymce/util/Tools", 16262 "tinymce/html/SaxParser" 16263 ], function(Env, Tools, SaxParser) { 16264 var trim = Tools.trim, trimContentRegExp; 16265 16266 trimContentRegExp = new RegExp([ 16267 '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers 16268 '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected 16269 ].join('|'), 'gi'); 16270 16271 return function(editor) { 16272 var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; 16273 16274 /** 16275 * Returns a trimmed version of the editor contents to be used for the undo level. This 16276 * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also 16277 * remove the data-mce-selected attributes used for selection of objects and caret containers. 16278 * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will 16279 * be removed by the serialization logic when you save. 16280 * 16281 * @private 16282 * @return {String} HTML contents of the editor excluding some internal bogus elements. 16283 */ 16284 function getContent() { 16285 var content = editor.getContent({format: 'raw', no_events: 1}); 16286 var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g; 16287 var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema; 16288 16289 content = content.replace(trimContentRegExp, ''); 16290 shortEndedElements = schema.getShortEndedElements(); 16291 16292 // Remove all bogus elements marked with "all" 16293 while ((matches = bogusAllRegExp.exec(content))) { 16294 index = bogusAllRegExp.lastIndex; 16295 matchLength = matches[0].length; 16296 16297 if (shortEndedElements[matches[1]]) { 16298 endTagIndex = index; 16299 } else { 16300 endTagIndex = SaxParser.findEndTag(schema, content, index); 16301 } 16302 16303 content = content.substring(0, index - matchLength) + content.substring(endTagIndex); 16304 bogusAllRegExp.lastIndex = index - matchLength; 16305 } 16306 16307 return trim(content); 16308 } 16309 16310 function addNonTypingUndoLevel(e) { 16311 self.typing = false; 16312 self.add({}, e); 16313 } 16314 16315 // Add initial undo level when the editor is initialized 16316 editor.on('init', function() { 16317 self.add(); 16318 }); 16319 16320 // Get position before an execCommand is processed 16321 editor.on('BeforeExecCommand', function(e) { 16322 var cmd = e.command; 16323 16324 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { 16325 self.beforeChange(); 16326 } 16327 }); 16328 16329 // Add undo level after an execCommand call was made 16330 editor.on('ExecCommand', function(e) { 16331 var cmd = e.command; 16332 16333 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { 16334 addNonTypingUndoLevel(e); 16335 } 16336 }); 16337 16338 editor.on('ObjectResizeStart', function() { 16339 self.beforeChange(); 16340 }); 16341 16342 editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); 16343 editor.on('DragEnd', addNonTypingUndoLevel); 16344 16345 editor.on('KeyUp', function(e) { 16346 var keyCode = e.keyCode; 16347 16348 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { 16349 addNonTypingUndoLevel(); 16350 editor.nodeChanged(); 16351 } 16352 16353 if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) { 16354 editor.nodeChanged(); 16355 } 16356 16357 // Fire a TypingUndo event on the first character entered 16358 if (isFirstTypedCharacter && self.typing) { 16359 // Make the it dirty if the content was changed after typing the first character 16360 if (!editor.isDirty()) { 16361 editor.isNotDirty = !data[0] || getContent() == data[0].content; 16362 16363 // Fire initial change event 16364 if (!editor.isNotDirty) { 16365 editor.fire('change', {level: data[0], lastLevel: null}); 16366 } 16367 } 16368 16369 editor.fire('TypingUndo'); 16370 isFirstTypedCharacter = false; 16371 editor.nodeChanged(); 16372 } 16373 }); 16374 16375 editor.on('KeyDown', function(e) { 16376 var keyCode = e.keyCode; 16377 16378 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter 16379 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { 16380 if (self.typing) { 16381 addNonTypingUndoLevel(e); 16382 } 16383 16384 return; 16385 } 16386 16387 // If key isn't shift,ctrl,alt,capslock,metakey 16388 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { 16389 self.beforeChange(); 16390 self.typing = true; 16391 self.add({}, e); 16392 isFirstTypedCharacter = true; 16393 } 16394 }); 16395 16396 editor.on('MouseDown', function(e) { 16397 if (self.typing) { 16398 addNonTypingUndoLevel(e); 16399 } 16400 }); 16401 16402 // Add keyboard shortcuts for undo/redo keys 16403 editor.addShortcut('ctrl+z', '', 'Undo'); 16404 editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo'); 16405 16406 editor.on('AddUndo Undo Redo ClearUndos', function(e) { 16407 if (!e.isDefaultPrevented()) { 16408 editor.nodeChanged(); 16409 } 16410 }); 16411 16412 self = { 16413 // Explose for debugging reasons 16414 data: data, 16415 16416 /** 16417 * State if the user is currently typing or not. This will add a typing operation into one undo 16418 * level instead of one new level for each keystroke. 16419 * 16420 * @field {Boolean} typing 16421 */ 16422 typing: false, 16423 16424 /** 16425 * Stores away a bookmark to be used when performing an undo action so that the selection is before 16426 * the change has been made. 16427 * 16428 * @method beforeChange 16429 */ 16430 beforeChange: function() { 16431 if (!locks) { 16432 beforeBookmark = editor.selection.getBookmark(2, true); 16433 } 16434 }, 16435 16436 /** 16437 * Adds a new undo level/snapshot to the undo list. 16438 * 16439 * @method add 16440 * @param {Object} level Optional undo level object to add. 16441 * @param {DOMEvent} Event Optional event responsible for the creation of the undo level. 16442 * @return {Object} Undo level that got added or null it a level wasn't needed. 16443 */ 16444 add: function(level, event) { 16445 var i, settings = editor.settings, lastLevel; 16446 16447 level = level || {}; 16448 level.content = getContent(); 16449 16450 if (locks || editor.removed) { 16451 return null; 16452 } 16453 16454 lastLevel = data[index]; 16455 if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) { 16456 return null; 16457 } 16458 16459 // Add undo level if needed 16460 if (lastLevel && lastLevel.content == level.content) { 16461 return null; 16462 } 16463 16464 // Set before bookmark on previous level 16465 if (data[index]) { 16466 data[index].beforeBookmark = beforeBookmark; 16467 } 16468 16469 // Time to compress 16470 if (settings.custom_undo_redo_levels) { 16471 if (data.length > settings.custom_undo_redo_levels) { 16472 for (i = 0; i < data.length - 1; i++) { 16473 data[i] = data[i + 1]; 16474 } 16475 16476 data.length--; 16477 index = data.length; 16478 } 16479 } 16480 16481 // Get a non intrusive normalized bookmark 16482 level.bookmark = editor.selection.getBookmark(2, true); 16483 16484 // Crop array if needed 16485 if (index < data.length - 1) { 16486 data.length = index + 1; 16487 } 16488 16489 data.push(level); 16490 index = data.length - 1; 16491 16492 var args = {level: level, lastLevel: lastLevel, originalEvent: event}; 16493 16494 editor.fire('AddUndo', args); 16495 16496 if (index > 0) { 16497 editor.isNotDirty = false; 16498 editor.fire('change', args); 16499 } 16500 16501 return level; 16502 }, 16503 16504 /** 16505 * Undoes the last action. 16506 * 16507 * @method undo 16508 * @return {Object} Undo level or null if no undo was performed. 16509 */ 16510 undo: function() { 16511 var level; 16512 16513 if (self.typing) { 16514 self.add(); 16515 self.typing = false; 16516 } 16517 16518 if (index > 0) { 16519 level = data[--index]; 16520 16521 // Undo to first index then set dirty state to false 16522 if (index === 0) { 16523 editor.isNotDirty = true; 16524 } 16525 16526 editor.setContent(level.content, {format: 'raw'}); 16527 editor.selection.moveToBookmark(level.beforeBookmark); 16528 16529 editor.fire('undo', {level: level}); 16530 } 16531 16532 return level; 16533 }, 16534 16535 /** 16536 * Redoes the last action. 16537 * 16538 * @method redo 16539 * @return {Object} Redo level or null if no redo was performed. 16540 */ 16541 redo: function() { 16542 var level; 16543 16544 if (index < data.length - 1) { 16545 level = data[++index]; 16546 16547 editor.setContent(level.content, {format: 'raw'}); 16548 editor.selection.moveToBookmark(level.bookmark); 16549 16550 editor.fire('redo', {level: level}); 16551 } 16552 16553 return level; 16554 }, 16555 16556 /** 16557 * Removes all undo levels. 16558 * 16559 * @method clear 16560 */ 16561 clear: function() { 16562 data = []; 16563 index = 0; 16564 self.typing = false; 16565 editor.fire('ClearUndos'); 16566 }, 16567 16568 /** 16569 * Returns true/false if the undo manager has any undo levels. 16570 * 16571 * @method hasUndo 16572 * @return {Boolean} true/false if the undo manager has any undo levels. 16573 */ 16574 hasUndo: function() { 16575 // Has undo levels or typing and content isn't the same as the initial level 16576 return index > 0 || (self.typing && data[0] && getContent() != data[0].content); 16577 }, 16578 16579 /** 16580 * Returns true/false if the undo manager has any redo levels. 16581 * 16582 * @method hasRedo 16583 * @return {Boolean} true/false if the undo manager has any redo levels. 16584 */ 16585 hasRedo: function() { 16586 return index < data.length - 1 && !this.typing; 16587 }, 16588 16589 /** 16590 * Executes the specified function in an undo transation. The selection 16591 * before the modification will be stored to the undo stack and if the DOM changes 16592 * it will add a new undo level. Any methods within the transation that adds undo levels will 16593 * be ignored. So a transation can include calls to execCommand or editor.insertContent. 16594 * 16595 * @method transact 16596 * @param {function} callback Function to execute dom manipulation logic in. 16597 */ 16598 transact: function(callback) { 16599 self.beforeChange(); 16600 16601 try { 16602 locks++; 16603 callback(); 16604 } finally { 16605 locks--; 16606 } 16607 16608 self.add(); 16609 } 16610 }; 16611 16612 return self; 16613 }; 16614 }); 16615 16616 // Included from: js/tinymce/classes/EnterKey.js 16617 16618 /** 16619 * EnterKey.js 16620 * 16621 * Copyright, Moxiecode Systems AB 16622 * Released under LGPL License. 16623 * 16624 * License: http://www.tinymce.com/license 16625 * Contributing: http://www.tinymce.com/contributing 16626 */ 16627 16628 /** 16629 * Contains logic for handling the enter key to split/generate block elements. 16630 */ 16631 define("tinymce/EnterKey", [ 16632 "tinymce/dom/TreeWalker", 16633 "tinymce/dom/RangeUtils", 16634 "tinymce/Env" 16635 ], function(TreeWalker, RangeUtils, Env) { 16636 var isIE = Env.ie && Env.ie < 11; 16637 16638 return function(editor) { 16639 var dom = editor.dom, selection = editor.selection, settings = editor.settings; 16640 var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); 16641 16642 function handleEnterKey(evt) { 16643 var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, 16644 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; 16645 16646 // Returns true if the block can be split into two blocks or not 16647 function canSplitBlock(node) { 16648 return node && 16649 dom.isBlock(node) && 16650 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 16651 !/^(fixed|absolute)/i.test(node.style.position) && 16652 dom.getContentEditable(node) !== "true"; 16653 } 16654 16655 // Renders empty block on IE 16656 function renderBlockOnIE(block) { 16657 var oldRng; 16658 16659 if (dom.isBlock(block)) { 16660 oldRng = selection.getRng(); 16661 block.appendChild(dom.create('span', null, '\u00a0')); 16662 selection.select(block); 16663 block.lastChild.outerHTML = ''; 16664 selection.setRng(oldRng); 16665 } 16666 } 16667 16668 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 16669 function trimInlineElementsOnLeftSideOfBlock(block) { 16670 var node = block, firstChilds = [], i; 16671 16672 if (!node) { 16673 return; 16674 } 16675 16676 // Find inner most first child ex: <p><i><b>*</b></i></p> 16677 while ((node = node.firstChild)) { 16678 if (dom.isBlock(node)) { 16679 return; 16680 } 16681 16682 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16683 firstChilds.push(node); 16684 } 16685 } 16686 16687 i = firstChilds.length; 16688 while (i--) { 16689 node = firstChilds[i]; 16690 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { 16691 dom.remove(node); 16692 } else { 16693 // Remove <a> </a> see #5381 16694 if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { 16695 dom.remove(node); 16696 } 16697 } 16698 } 16699 } 16700 16701 // Moves the caret to a suitable position within the root for example in the first non 16702 // pure whitespace text node or before an image 16703 function moveToCaretPosition(root) { 16704 var walker, node, rng, lastNode = root, tempElm; 16705 16706 function firstNonWhiteSpaceNodeSibling(node) { 16707 while (node) { 16708 if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) { 16709 return node; 16710 } 16711 16712 node = node.nextSibling; 16713 } 16714 } 16715 16716 if (!root) { 16717 return; 16718 } 16719 16720 // Old IE versions doesn't properly render blocks with br elements in them 16721 // For example <p><br></p> wont be rendered correctly in a contentEditable area 16722 // until you remove the br producing <p></p> 16723 if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) { 16724 if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') { 16725 dom.remove(parentBlock.firstChild); 16726 } 16727 } 16728 16729 if (/^(LI|DT|DD)$/.test(root.nodeName)) { 16730 var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); 16731 16732 if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) { 16733 root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); 16734 } 16735 } 16736 16737 rng = dom.createRng(); 16738 16739 // Normalize whitespace to remove empty text nodes. Fix for: #6904 16740 // Gecko will be able to place the caret in empty text nodes but it won't render propery 16741 // Older IE versions will sometimes crash so for now ignore all IE versions 16742 if (!Env.ie) { 16743 root.normalize(); 16744 } 16745 16746 if (root.hasChildNodes()) { 16747 walker = new TreeWalker(root, root); 16748 16749 while ((node = walker.current())) { 16750 if (node.nodeType == 3) { 16751 rng.setStart(node, 0); 16752 rng.setEnd(node, 0); 16753 break; 16754 } 16755 16756 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16757 rng.setStartBefore(node); 16758 rng.setEndBefore(node); 16759 break; 16760 } 16761 16762 lastNode = node; 16763 node = walker.next(); 16764 } 16765 16766 if (!node) { 16767 rng.setStart(lastNode, 0); 16768 rng.setEnd(lastNode, 0); 16769 } 16770 } else { 16771 if (root.nodeName == 'BR') { 16772 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 16773 // Trick on older IE versions to render the caret before the BR between two lists 16774 if (!documentMode || documentMode < 9) { 16775 tempElm = dom.create('br'); 16776 root.parentNode.insertBefore(tempElm, root); 16777 } 16778 16779 rng.setStartBefore(root); 16780 rng.setEndBefore(root); 16781 } else { 16782 rng.setStartAfter(root); 16783 rng.setEndAfter(root); 16784 } 16785 } else { 16786 rng.setStart(root, 0); 16787 rng.setEnd(root, 0); 16788 } 16789 } 16790 16791 selection.setRng(rng); 16792 16793 // Remove tempElm created for old IE:s 16794 dom.remove(tempElm); 16795 selection.scrollIntoView(root); 16796 } 16797 16798 function setForcedBlockAttrs(node) { 16799 var forcedRootBlockName = settings.forced_root_block; 16800 16801 if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { 16802 dom.setAttribs(node, settings.forced_root_block_attrs); 16803 } 16804 } 16805 16806 // Creates a new block element by cloning the current one or creating a new one if the name is specified 16807 // This function will also copy any text formatting from the parent block and add it to the new one 16808 function createNewBlock(name) { 16809 var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements(); 16810 16811 if (name || parentBlockName == "TABLE") { 16812 block = dom.create(name || newBlockName); 16813 setForcedBlockAttrs(block); 16814 } else { 16815 block = parentBlock.cloneNode(false); 16816 } 16817 16818 caretNode = block; 16819 16820 // Clone any parent styles 16821 if (settings.keep_styles !== false) { 16822 do { 16823 if (textInlineElements[node.nodeName]) { 16824 // Never clone a caret containers 16825 if (node.id == '_mce_caret') { 16826 continue; 16827 } 16828 16829 clonedNode = node.cloneNode(false); 16830 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 16831 16832 if (block.hasChildNodes()) { 16833 clonedNode.appendChild(block.firstChild); 16834 block.appendChild(clonedNode); 16835 } else { 16836 caretNode = clonedNode; 16837 block.appendChild(clonedNode); 16838 } 16839 } 16840 } while ((node = node.parentNode)); 16841 } 16842 16843 // BR is needed in empty blocks on non IE browsers 16844 if (!isIE) { 16845 caretNode.innerHTML = '<br data-mce-bogus="1">'; 16846 } 16847 16848 return block; 16849 } 16850 16851 // Returns true/false if the caret is at the start/end of the parent block element 16852 function isCaretAtStartOrEndOfBlock(start) { 16853 var walker, node, name; 16854 16855 // Caret is in the middle of a text node like "a|b" 16856 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { 16857 return false; 16858 } 16859 16860 // If after the last element in block node edge case for #5091 16861 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { 16862 return true; 16863 } 16864 16865 // If the caret if before the first element in parentBlock 16866 if (start && container.nodeType == 1 && container == parentBlock.firstChild) { 16867 return true; 16868 } 16869 16870 // Caret can be before/after a table 16871 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { 16872 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 16873 } 16874 16875 // Walk the DOM and look for text nodes or non empty elements 16876 walker = new TreeWalker(container, parentBlock); 16877 16878 // If caret is in beginning or end of a text block then jump to the next/previous node 16879 if (container.nodeType == 3) { 16880 if (start && offset === 0) { 16881 walker.prev(); 16882 } else if (!start && offset == container.nodeValue.length) { 16883 walker.next(); 16884 } 16885 } 16886 16887 while ((node = walker.current())) { 16888 if (node.nodeType === 1) { 16889 // Ignore bogus elements 16890 if (!node.getAttribute('data-mce-bogus')) { 16891 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 16892 name = node.nodeName.toLowerCase(); 16893 if (nonEmptyElementsMap[name] && name !== 'br') { 16894 return false; 16895 } 16896 } 16897 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { 16898 return false; 16899 } 16900 16901 if (start) { 16902 walker.prev(); 16903 } else { 16904 walker.next(); 16905 } 16906 } 16907 16908 return true; 16909 } 16910 16911 // Wraps any text nodes or inline elements in the specified forced root block name 16912 function wrapSelfAndSiblingsInDefaultBlock(container, offset) { 16913 var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P'; 16914 16915 // Not in a block element or in a table cell or caption 16916 parentBlock = dom.getParent(container, dom.isBlock); 16917 rootBlockName = editor.getBody().nodeName.toLowerCase(); 16918 if (!parentBlock || !canSplitBlock(parentBlock)) { 16919 parentBlock = parentBlock || editableRoot; 16920 16921 if (!parentBlock.hasChildNodes()) { 16922 newBlock = dom.create(blockName); 16923 setForcedBlockAttrs(newBlock); 16924 parentBlock.appendChild(newBlock); 16925 rng.setStart(newBlock, 0); 16926 rng.setEnd(newBlock, 0); 16927 return newBlock; 16928 } 16929 16930 // Find parent that is the first child of parentBlock 16931 node = container; 16932 while (node.parentNode != parentBlock) { 16933 node = node.parentNode; 16934 } 16935 16936 // Loop left to find start node start wrapping at 16937 while (node && !dom.isBlock(node)) { 16938 startNode = node; 16939 node = node.previousSibling; 16940 } 16941 16942 if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) { 16943 newBlock = dom.create(blockName); 16944 setForcedBlockAttrs(newBlock); 16945 startNode.parentNode.insertBefore(newBlock, startNode); 16946 16947 // Start wrapping until we hit a block 16948 node = startNode; 16949 while (node && !dom.isBlock(node)) { 16950 next = node.nextSibling; 16951 newBlock.appendChild(node); 16952 node = next; 16953 } 16954 16955 // Restore range to it's past location 16956 rng.setStart(container, offset); 16957 rng.setEnd(container, offset); 16958 } 16959 } 16960 16961 return container; 16962 } 16963 16964 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 16965 function handleEmptyListItem() { 16966 function isFirstOrLastLi(first) { 16967 var node = containerBlock[first ? 'firstChild' : 'lastChild']; 16968 16969 // Find first/last element since there might be whitespace there 16970 while (node) { 16971 if (node.nodeType == 1) { 16972 break; 16973 } 16974 16975 node = node[first ? 'nextSibling' : 'previousSibling']; 16976 } 16977 16978 return node === parentBlock; 16979 } 16980 16981 function getContainerBlock() { 16982 var containerBlockParent = containerBlock.parentNode; 16983 16984 if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { 16985 return containerBlockParent; 16986 } 16987 16988 return containerBlock; 16989 } 16990 16991 // Check if we are in an nested list 16992 var containerBlockParentName = containerBlock.parentNode.nodeName; 16993 if (/^(OL|UL|LI)$/.test(containerBlockParentName)) { 16994 newBlockName = 'LI'; 16995 } 16996 16997 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); 16998 16999 if (isFirstOrLastLi(true) && isFirstOrLastLi()) { 17000 if (containerBlockParentName == 'LI') { 17001 // Nested list is inside a LI 17002 dom.insertAfter(newBlock, getContainerBlock()); 17003 } else { 17004 // Is first and last list item then replace the OL/UL with a text block 17005 dom.replace(newBlock, containerBlock); 17006 } 17007 } else if (isFirstOrLastLi(true)) { 17008 if (containerBlockParentName == 'LI') { 17009 // List nested in an LI then move the list to a new sibling LI 17010 dom.insertAfter(newBlock, getContainerBlock()); 17011 newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed 17012 newBlock.appendChild(containerBlock); 17013 } else { 17014 // First LI in list then remove LI and add text block before list 17015 containerBlock.parentNode.insertBefore(newBlock, containerBlock); 17016 } 17017 } else if (isFirstOrLastLi()) { 17018 // Last LI in list then remove LI and add text block after list 17019 dom.insertAfter(newBlock, getContainerBlock()); 17020 renderBlockOnIE(newBlock); 17021 } else { 17022 // Middle LI in list the split the list and insert a text block in the middle 17023 // Extract after fragment and insert it after the current block 17024 containerBlock = getContainerBlock(); 17025 tmpRng = rng.cloneRange(); 17026 tmpRng.setStartAfter(parentBlock); 17027 tmpRng.setEndAfter(containerBlock); 17028 fragment = tmpRng.extractContents(); 17029 17030 if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') { 17031 newBlock = fragment.firstChild; 17032 dom.insertAfter(fragment, containerBlock); 17033 } else { 17034 dom.insertAfter(fragment, containerBlock); 17035 dom.insertAfter(newBlock, containerBlock); 17036 } 17037 } 17038 17039 dom.remove(parentBlock); 17040 moveToCaretPosition(newBlock); 17041 undoManager.add(); 17042 } 17043 17044 // Inserts a BR element if the forced_root_block option is set to false or empty string 17045 function insertBr() { 17046 editor.execCommand("InsertLineBreak", false, evt); 17047 } 17048 17049 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 17050 function trimLeadingLineBreaks(node) { 17051 do { 17052 if (node.nodeType === 3) { 17053 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); 17054 } 17055 17056 node = node.firstChild; 17057 } while (node); 17058 } 17059 17060 function getEditableRoot(node) { 17061 var root = dom.getRoot(), parent, editableRoot; 17062 17063 // Get all parents until we hit a non editable parent or the root 17064 parent = node; 17065 while (parent !== root && dom.getContentEditable(parent) !== "false") { 17066 if (dom.getContentEditable(parent) === "true") { 17067 editableRoot = parent; 17068 } 17069 17070 parent = parent.parentNode; 17071 } 17072 17073 return parent !== root ? editableRoot : root; 17074 } 17075 17076 // Adds a BR at the end of blocks that only contains an IMG or INPUT since 17077 // these might be floated and then they won't expand the block 17078 function addBrToBlockIfNeeded(block) { 17079 var lastChild; 17080 17081 // IE will render the blocks correctly other browsers needs a BR 17082 if (!isIE) { 17083 block.normalize(); // Remove empty text nodes that got left behind by the extract 17084 17085 // Check if the block is empty or contains a floated last child 17086 lastChild = block.lastChild; 17087 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 17088 dom.add(block, 'br'); 17089 } 17090 } 17091 } 17092 17093 rng = selection.getRng(true); 17094 17095 // Event is blocked by some other handler for example the lists plugin 17096 if (evt.isDefaultPrevented()) { 17097 return; 17098 } 17099 17100 // Delete any selected contents 17101 if (!rng.collapsed) { 17102 editor.execCommand('Delete'); 17103 return; 17104 } 17105 17106 // Setup range items and newBlockName 17107 new RangeUtils(dom).normalize(rng); 17108 container = rng.startContainer; 17109 offset = rng.startOffset; 17110 newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; 17111 newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; 17112 documentMode = dom.doc.documentMode; 17113 shiftKey = evt.shiftKey; 17114 17115 // Resolve node index 17116 if (container.nodeType == 1 && container.hasChildNodes()) { 17117 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 17118 17119 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 17120 if (isAfterLastNodeInContainer && container.nodeType == 3) { 17121 offset = container.nodeValue.length; 17122 } else { 17123 offset = 0; 17124 } 17125 } 17126 17127 // Get editable root node normaly the body element but sometimes a div or span 17128 editableRoot = getEditableRoot(container); 17129 17130 // If there is no editable root then enter is done inside a contentEditable false element 17131 if (!editableRoot) { 17132 return; 17133 } 17134 17135 undoManager.beforeChange(); 17136 17137 // If editable root isn't block nor the root of the editor 17138 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { 17139 if (!newBlockName || shiftKey) { 17140 insertBr(); 17141 } 17142 17143 return; 17144 } 17145 17146 // Wrap the current node and it's sibling in a default block if it's needed. 17147 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 17148 // This won't happen if root blocks are disabled or the shiftKey is pressed 17149 if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { 17150 container = wrapSelfAndSiblingsInDefaultBlock(container, offset); 17151 } 17152 17153 // Find parent block and setup empty block paddings 17154 parentBlock = dom.getParent(container, dom.isBlock); 17155 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 17156 17157 // Setup block names 17158 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17159 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17160 17161 // Enter inside block contained within a LI then split or insert before/after LI 17162 if (containerBlockName == 'LI' && !evt.ctrlKey) { 17163 parentBlock = containerBlock; 17164 parentBlockName = containerBlockName; 17165 } 17166 17167 // Handle enter in list item 17168 if (/^(LI|DT|DD)$/.test(parentBlockName)) { 17169 if (!newBlockName && shiftKey) { 17170 insertBr(); 17171 return; 17172 } 17173 17174 // Handle enter inside an empty list item 17175 if (dom.isEmpty(parentBlock)) { 17176 handleEmptyListItem(); 17177 return; 17178 } 17179 } 17180 17181 // Don't split PRE tags but insert a BR instead easier when writing code samples etc 17182 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { 17183 if (!shiftKey) { 17184 insertBr(); 17185 return; 17186 } 17187 } else { 17188 // If no root block is configured then insert a BR by default or if the shiftKey is pressed 17189 if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { 17190 insertBr(); 17191 return; 17192 } 17193 } 17194 17195 // If parent block is root then never insert new blocks 17196 if (newBlockName && parentBlock === editor.getBody()) { 17197 return; 17198 } 17199 17200 // Default block name if it's not configured 17201 newBlockName = newBlockName || 'P'; 17202 17203 // Insert new block before/after the parent block depending on caret location 17204 if (isCaretAtStartOrEndOfBlock()) { 17205 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 17206 if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { 17207 newBlock = createNewBlock(newBlockName); 17208 } else { 17209 newBlock = createNewBlock(); 17210 } 17211 17212 // Split the current container block element if enter is pressed inside an empty inner block element 17213 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { 17214 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 17215 newBlock = dom.split(containerBlock, parentBlock); 17216 } else { 17217 dom.insertAfter(newBlock, parentBlock); 17218 } 17219 17220 moveToCaretPosition(newBlock); 17221 } else if (isCaretAtStartOrEndOfBlock(true)) { 17222 // Insert new block before 17223 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); 17224 renderBlockOnIE(newBlock); 17225 moveToCaretPosition(parentBlock); 17226 } else { 17227 // Extract after fragment and insert it after the current block 17228 tmpRng = rng.cloneRange(); 17229 tmpRng.setEndAfter(parentBlock); 17230 fragment = tmpRng.extractContents(); 17231 trimLeadingLineBreaks(fragment); 17232 newBlock = fragment.firstChild; 17233 dom.insertAfter(fragment, parentBlock); 17234 trimInlineElementsOnLeftSideOfBlock(newBlock); 17235 addBrToBlockIfNeeded(parentBlock); 17236 moveToCaretPosition(newBlock); 17237 } 17238 17239 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 17240 17241 // Allow custom handling of new blocks 17242 editor.fire('NewBlock', {newBlock: newBlock}); 17243 17244 undoManager.add(); 17245 } 17246 17247 editor.on('keydown', function(evt) { 17248 if (evt.keyCode == 13) { 17249 if (handleEnterKey(evt) !== false) { 17250 evt.preventDefault(); 17251 } 17252 } 17253 }); 17254 }; 17255 }); 17256 17257 // Included from: js/tinymce/classes/ForceBlocks.js 17258 17259 /** 17260 * ForceBlocks.js 17261 * 17262 * Copyright, Moxiecode Systems AB 17263 * Released under LGPL License. 17264 * 17265 * License: http://www.tinymce.com/license 17266 * Contributing: http://www.tinymce.com/contributing 17267 */ 17268 17269 define("tinymce/ForceBlocks", [], function() { 17270 return function(editor) { 17271 var settings = editor.settings, dom = editor.dom, selection = editor.selection; 17272 var schema = editor.schema, blockElements = schema.getBlockElements(); 17273 17274 function addRootBlocks() { 17275 var node = selection.getStart(), rootNode = editor.getBody(), rng; 17276 var startContainer, startOffset, endContainer, endOffset, rootBlockNode; 17277 var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection; 17278 var tmpRng, rootNodeName, forcedRootBlock; 17279 17280 forcedRootBlock = settings.forced_root_block; 17281 17282 if (!node || node.nodeType !== 1 || !forcedRootBlock) { 17283 return; 17284 } 17285 17286 // Check if node is wrapped in block 17287 while (node && node != rootNode) { 17288 if (blockElements[node.nodeName]) { 17289 return; 17290 } 17291 17292 node = node.parentNode; 17293 } 17294 17295 // Get current selection 17296 rng = selection.getRng(); 17297 if (rng.setStart) { 17298 startContainer = rng.startContainer; 17299 startOffset = rng.startOffset; 17300 endContainer = rng.endContainer; 17301 endOffset = rng.endOffset; 17302 17303 try { 17304 restoreSelection = editor.getDoc().activeElement === rootNode; 17305 } catch (ex) { 17306 // IE throws unspecified error here sometimes 17307 } 17308 } else { 17309 // Force control range into text range 17310 if (rng.item) { 17311 node = rng.item(0); 17312 rng = editor.getDoc().body.createTextRange(); 17313 rng.moveToElementText(node); 17314 } 17315 17316 restoreSelection = rng.parentElement().ownerDocument === editor.getDoc(); 17317 tmpRng = rng.duplicate(); 17318 tmpRng.collapse(true); 17319 startOffset = tmpRng.move('character', offset) * -1; 17320 17321 if (!tmpRng.collapsed) { 17322 tmpRng = rng.duplicate(); 17323 tmpRng.collapse(false); 17324 endOffset = (tmpRng.move('character', offset) * -1) - startOffset; 17325 } 17326 } 17327 17328 // Wrap non block elements and text nodes 17329 node = rootNode.firstChild; 17330 rootNodeName = rootNode.nodeName.toLowerCase(); 17331 while (node) { 17332 // TODO: Break this up, too complex 17333 if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) && 17334 schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) { 17335 // Remove empty text nodes 17336 if (node.nodeType === 3 && node.nodeValue.length === 0) { 17337 tempNode = node; 17338 node = node.nextSibling; 17339 dom.remove(tempNode); 17340 continue; 17341 } 17342 17343 if (!rootBlockNode) { 17344 rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs); 17345 node.parentNode.insertBefore(rootBlockNode, node); 17346 wrapped = true; 17347 } 17348 17349 tempNode = node; 17350 node = node.nextSibling; 17351 rootBlockNode.appendChild(tempNode); 17352 } else { 17353 rootBlockNode = null; 17354 node = node.nextSibling; 17355 } 17356 } 17357 17358 if (wrapped && restoreSelection) { 17359 if (rng.setStart) { 17360 rng.setStart(startContainer, startOffset); 17361 rng.setEnd(endContainer, endOffset); 17362 selection.setRng(rng); 17363 } else { 17364 // Only select if the previous selection was inside the document to prevent auto focus in quirks mode 17365 try { 17366 rng = editor.getDoc().body.createTextRange(); 17367 rng.moveToElementText(rootNode); 17368 rng.collapse(true); 17369 rng.moveStart('character', startOffset); 17370 17371 if (endOffset > 0) { 17372 rng.moveEnd('character', endOffset); 17373 } 17374 17375 rng.select(); 17376 } catch (ex) { 17377 // Ignore 17378 } 17379 } 17380 17381 editor.nodeChanged(); 17382 } 17383 } 17384 17385 // Force root blocks 17386 if (settings.forced_root_block) { 17387 editor.on('NodeChange', addRootBlocks); 17388 } 17389 }; 17390 }); 17391 17392 // Included from: js/tinymce/classes/EditorCommands.js 17393 17394 /** 17395 * EditorCommands.js 17396 * 17397 * Copyright, Moxiecode Systems AB 17398 * Released under LGPL License. 17399 * 17400 * License: http://www.tinymce.com/license 17401 * Contributing: http://www.tinymce.com/contributing 17402 */ 17403 17404 /** 17405 * This class enables you to add custom editor commands and it contains 17406 * overrides for native browser commands to address various bugs and issues. 17407 * 17408 * @class tinymce.EditorCommands 17409 */ 17410 define("tinymce/EditorCommands", [ 17411 "tinymce/html/Serializer", 17412 "tinymce/Env", 17413 "tinymce/util/Tools", 17414 "tinymce/dom/ElementUtils", 17415 "tinymce/dom/RangeUtils", 17416 "tinymce/dom/TreeWalker" 17417 ], function(Serializer, Env, Tools, ElementUtils, RangeUtils, TreeWalker) { 17418 // Added for compression purposes 17419 var each = Tools.each, extend = Tools.extend; 17420 var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; 17421 var isGecko = Env.gecko, isIE = Env.ie, isOldIE = Env.ie && Env.ie < 11; 17422 var TRUE = true, FALSE = false; 17423 17424 return function(editor) { 17425 var dom = editor.dom, 17426 selection = editor.selection, 17427 commands = {state: {}, exec: {}, value: {}}, 17428 settings = editor.settings, 17429 formatter = editor.formatter, 17430 bookmark; 17431 17432 /** 17433 * Executes the specified command. 17434 * 17435 * @method execCommand 17436 * @param {String} command Command to execute. 17437 * @param {Boolean} ui Optional user interface state. 17438 * @param {Object} value Optional value for command. 17439 * @return {Boolean} true/false if the command was found or not. 17440 */ 17441 function execCommand(command, ui, value) { 17442 var func; 17443 17444 command = command.toLowerCase(); 17445 if ((func = commands.exec[command])) { 17446 func(command, ui, value); 17447 return TRUE; 17448 } 17449 17450 return FALSE; 17451 } 17452 17453 /** 17454 * Queries the current state for a command for example if the current selection is "bold". 17455 * 17456 * @method queryCommandState 17457 * @param {String} command Command to check the state of. 17458 * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. 17459 */ 17460 function queryCommandState(command) { 17461 var func; 17462 17463 command = command.toLowerCase(); 17464 if ((func = commands.state[command])) { 17465 return func(command); 17466 } 17467 17468 return -1; 17469 } 17470 17471 /** 17472 * Queries the command value for example the current fontsize. 17473 * 17474 * @method queryCommandValue 17475 * @param {String} command Command to check the value of. 17476 * @return {Object} Command value of false if it's not found. 17477 */ 17478 function queryCommandValue(command) { 17479 var func; 17480 17481 command = command.toLowerCase(); 17482 if ((func = commands.value[command])) { 17483 return func(command); 17484 } 17485 17486 return FALSE; 17487 } 17488 17489 /** 17490 * Adds commands to the command collection. 17491 * 17492 * @method addCommands 17493 * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated. 17494 * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. 17495 */ 17496 function addCommands(command_list, type) { 17497 type = type || 'exec'; 17498 17499 each(command_list, function(callback, command) { 17500 each(command.toLowerCase().split(','), function(command) { 17501 commands[type][command] = callback; 17502 }); 17503 }); 17504 } 17505 17506 // Expose public methods 17507 extend(this, { 17508 execCommand: execCommand, 17509 queryCommandState: queryCommandState, 17510 queryCommandValue: queryCommandValue, 17511 addCommands: addCommands 17512 }); 17513 17514 // Private methods 17515 17516 function execNativeCommand(command, ui, value) { 17517 if (ui === undefined) { 17518 ui = FALSE; 17519 } 17520 17521 if (value === undefined) { 17522 value = null; 17523 } 17524 17525 return editor.getDoc().execCommand(command, ui, value); 17526 } 17527 17528 function isFormatMatch(name) { 17529 return formatter.match(name); 17530 } 17531 17532 function toggleFormat(name, value) { 17533 formatter.toggle(name, value ? {value: value} : undefined); 17534 editor.nodeChanged(); 17535 } 17536 17537 function storeSelection(type) { 17538 bookmark = selection.getBookmark(type); 17539 } 17540 17541 function restoreSelection() { 17542 selection.moveToBookmark(bookmark); 17543 } 17544 17545 // Add execCommand overrides 17546 addCommands({ 17547 // Ignore these, added for compatibility 17548 'mceResetDesignMode,mceBeginUndoLevel': function() {}, 17549 17550 // Add undo manager logic 17551 'mceEndUndoLevel,mceAddUndoLevel': function() { 17552 editor.undoManager.add(); 17553 }, 17554 17555 'Cut,Copy,Paste': function(command) { 17556 var doc = editor.getDoc(), failed; 17557 17558 // Try executing the native command 17559 try { 17560 execNativeCommand(command); 17561 } catch (ex) { 17562 // Command failed 17563 failed = TRUE; 17564 } 17565 17566 // Present alert message about clipboard access not being available 17567 if (failed || !doc.queryCommandSupported(command)) { 17568 var msg = editor.translate( 17569 "Your browser doesn't support direct access to the clipboard. " + 17570 "Please use the Ctrl+X/C/V keyboard shortcuts instead." 17571 ); 17572 17573 if (Env.mac) { 17574 msg = msg.replace(/Ctrl\+/g, '\u2318+'); 17575 } 17576 17577 editor.windowManager.alert(msg); 17578 } 17579 }, 17580 17581 // Override unlink command 17582 unlink: function() { 17583 if (selection.isCollapsed()) { 17584 var elm = selection.getNode(); 17585 if (elm.tagName == 'A') { 17586 editor.dom.remove(elm, true); 17587 } 17588 17589 return; 17590 } 17591 17592 formatter.remove("link"); 17593 }, 17594 17595 // Override justify commands to use the text formatter engine 17596 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { 17597 var align = command.substring(7); 17598 17599 if (align == 'full') { 17600 align = 'justify'; 17601 } 17602 17603 // Remove all other alignments first 17604 each('left,center,right,justify'.split(','), function(name) { 17605 if (align != name) { 17606 formatter.remove('align' + name); 17607 } 17608 }); 17609 17610 toggleFormat('align' + align); 17611 execCommand('mceRepaint'); 17612 }, 17613 17614 // Override list commands to fix WebKit bug 17615 'InsertUnorderedList,InsertOrderedList': function(command) { 17616 var listElm, listParent; 17617 17618 execNativeCommand(command); 17619 17620 // WebKit produces lists within block elements so we need to split them 17621 // we will replace the native list creation logic to custom logic later on 17622 // TODO: Remove this when the list creation logic is removed 17623 listElm = dom.getParent(selection.getNode(), 'ol,ul'); 17624 if (listElm) { 17625 listParent = listElm.parentNode; 17626 17627 // If list is within a text block then split that block 17628 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { 17629 storeSelection(); 17630 dom.split(listParent, listElm); 17631 restoreSelection(); 17632 } 17633 } 17634 }, 17635 17636 // Override commands to use the text formatter engine 17637 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { 17638 toggleFormat(command); 17639 }, 17640 17641 // Override commands to use the text formatter engine 17642 'ForeColor,HiliteColor,FontName': function(command, ui, value) { 17643 toggleFormat(command, value); 17644 }, 17645 17646 FontSize: function(command, ui, value) { 17647 var fontClasses, fontSizes; 17648 17649 // Convert font size 1-7 to styles 17650 if (value >= 1 && value <= 7) { 17651 fontSizes = explode(settings.font_size_style_values); 17652 fontClasses = explode(settings.font_size_classes); 17653 17654 if (fontClasses) { 17655 value = fontClasses[value - 1] || value; 17656 } else { 17657 value = fontSizes[value - 1] || value; 17658 } 17659 } 17660 17661 toggleFormat(command, value); 17662 }, 17663 17664 RemoveFormat: function(command) { 17665 formatter.remove(command); 17666 }, 17667 17668 mceBlockQuote: function() { 17669 toggleFormat('blockquote'); 17670 }, 17671 17672 FormatBlock: function(command, ui, value) { 17673 return toggleFormat(value || 'p'); 17674 }, 17675 17676 mceCleanup: function() { 17677 var bookmark = selection.getBookmark(); 17678 17679 editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE}); 17680 17681 selection.moveToBookmark(bookmark); 17682 }, 17683 17684 mceRemoveNode: function(command, ui, value) { 17685 var node = value || selection.getNode(); 17686 17687 // Make sure that the body node isn't removed 17688 if (node != editor.getBody()) { 17689 storeSelection(); 17690 editor.dom.remove(node, TRUE); 17691 restoreSelection(); 17692 } 17693 }, 17694 17695 mceSelectNodeDepth: function(command, ui, value) { 17696 var counter = 0; 17697 17698 dom.getParent(selection.getNode(), function(node) { 17699 if (node.nodeType == 1 && counter++ == value) { 17700 selection.select(node); 17701 return FALSE; 17702 } 17703 }, editor.getBody()); 17704 }, 17705 17706 mceSelectNode: function(command, ui, value) { 17707 selection.select(value); 17708 }, 17709 17710 mceInsertContent: function(command, ui, value) { 17711 var parser, serializer, parentNode, rootNode, fragment, args; 17712 var marker, rng, node, node2, bookmarkHtml, merge; 17713 var textInlineElements = editor.schema.getTextInlineElements(); 17714 17715 function trimOrPaddLeftRight(html) { 17716 var rng, container, offset; 17717 17718 rng = selection.getRng(true); 17719 container = rng.startContainer; 17720 offset = rng.startOffset; 17721 17722 function hasSiblingText(siblingName) { 17723 return container[siblingName] && container[siblingName].nodeType == 3; 17724 } 17725 17726 if (container.nodeType == 3) { 17727 if (offset > 0) { 17728 html = html.replace(/^ /, ' '); 17729 } else if (!hasSiblingText('previousSibling')) { 17730 html = html.replace(/^ /, ' '); 17731 } 17732 17733 if (offset < container.length) { 17734 html = html.replace(/ (<br>|)$/, ' '); 17735 } else if (!hasSiblingText('nextSibling')) { 17736 html = html.replace(/( | )(<br>|)$/, ' '); 17737 } 17738 } 17739 17740 return html; 17741 } 17742 17743 function markInlineFormatElements(fragment) { 17744 if (merge) { 17745 for (node = fragment.firstChild; node; node = node.walk(true)) { 17746 if (textInlineElements[node.name]) { 17747 node.attr('data-mce-new', "true"); 17748 } 17749 } 17750 } 17751 } 17752 17753 function reduceInlineTextElements() { 17754 if (merge) { 17755 var root = editor.getBody(), elementUtils = new ElementUtils(dom); 17756 17757 each(dom.select('*[data-mce-new]'), function(node) { 17758 node.removeAttribute('data-mce-new'); 17759 17760 for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) { 17761 if (elementUtils.compare(testNode, node)) { 17762 dom.remove(node, true); 17763 } 17764 } 17765 }); 17766 } 17767 } 17768 17769 if (typeof(value) != 'string') { 17770 merge = value.merge; 17771 value = value.content; 17772 } 17773 17774 // Check for whitespace before/after value 17775 if (/^ | $/.test(value)) { 17776 value = trimOrPaddLeftRight(value); 17777 } 17778 17779 // Setup parser and serializer 17780 parser = editor.parser; 17781 serializer = new Serializer({}, editor.schema); 17782 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark"></span>'; 17783 17784 // Run beforeSetContent handlers on the HTML to be inserted 17785 args = {content: value, format: 'html', selection: true}; 17786 editor.fire('BeforeSetContent', args); 17787 value = args.content; 17788 17789 // Add caret at end of contents if it's missing 17790 if (value.indexOf('{$caret}') == -1) { 17791 value += '{$caret}'; 17792 } 17793 17794 // Replace the caret marker with a span bookmark element 17795 value = value.replace(/\{\$caret\}/, bookmarkHtml); 17796 17797 // If selection is at <body>|<p></p> then move it into <body><p>|</p> 17798 rng = selection.getRng(); 17799 var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); 17800 var body = editor.getBody(); 17801 if (caretElement === body && selection.isCollapsed()) { 17802 if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) { 17803 rng = dom.createRng(); 17804 rng.setStart(body.firstChild, 0); 17805 rng.setEnd(body.firstChild, 0); 17806 selection.setRng(rng); 17807 } 17808 } 17809 17810 // Insert node maker where we will insert the new HTML and get it's parent 17811 if (!selection.isCollapsed()) { 17812 editor.getDoc().execCommand('Delete', false, null); 17813 } 17814 17815 parentNode = selection.getNode(); 17816 17817 // Parse the fragment within the context of the parent node 17818 var parserArgs = {context: parentNode.nodeName.toLowerCase()}; 17819 fragment = parser.parse(value, parserArgs); 17820 17821 markInlineFormatElements(fragment); 17822 17823 // Move the caret to a more suitable location 17824 node = fragment.lastChild; 17825 if (node.attr('id') == 'mce_marker') { 17826 marker = node; 17827 17828 for (node = node.prev; node; node = node.walk(true)) { 17829 if (node.type == 3 || !dom.isBlock(node.name)) { 17830 if (editor.schema.isValidChild(node.parent.name, 'span')) { 17831 node.parent.insert(marker, node, node.name === 'br'); 17832 } 17833 break; 17834 } 17835 } 17836 } 17837 17838 // If parser says valid we can insert the contents into that parent 17839 if (!parserArgs.invalid) { 17840 value = serializer.serialize(fragment); 17841 17842 // Check if parent is empty or only has one BR element then set the innerHTML of that parent 17843 node = parentNode.firstChild; 17844 node2 = parentNode.lastChild; 17845 if (!node || (node === node2 && node.nodeName === 'BR')) { 17846 dom.setHTML(parentNode, value); 17847 } else { 17848 selection.setContent(value); 17849 } 17850 } else { 17851 // If the fragment was invalid within that context then we need 17852 // to parse and process the parent it's inserted into 17853 17854 // Insert bookmark node and get the parent 17855 selection.setContent(bookmarkHtml); 17856 parentNode = selection.getNode(); 17857 rootNode = editor.getBody(); 17858 17859 // Opera will return the document node when selection is in root 17860 if (parentNode.nodeType == 9) { 17861 parentNode = node = rootNode; 17862 } else { 17863 node = parentNode; 17864 } 17865 17866 // Find the ancestor just before the root element 17867 while (node !== rootNode) { 17868 parentNode = node; 17869 node = node.parentNode; 17870 } 17871 17872 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 17873 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 17874 value = serializer.serialize( 17875 parser.parse( 17876 // Need to replace by using a function since $ in the contents would otherwise be a problem 17877 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() { 17878 return serializer.serialize(fragment); 17879 }) 17880 ) 17881 ); 17882 17883 // Set the inner/outer HTML depending on if we are in the root or not 17884 if (parentNode == rootNode) { 17885 dom.setHTML(rootNode, value); 17886 } else { 17887 dom.setOuterHTML(parentNode, value); 17888 } 17889 } 17890 17891 reduceInlineTextElements(); 17892 17893 marker = dom.get('mce_marker'); 17894 selection.scrollIntoView(marker); 17895 17896 // Move selection before marker and remove it 17897 rng = dom.createRng(); 17898 17899 // If previous sibling is a text node set the selection to the end of that node 17900 node = marker.previousSibling; 17901 if (node && node.nodeType == 3) { 17902 rng.setStart(node, node.nodeValue.length); 17903 17904 // TODO: Why can't we normalize on IE 17905 if (!isIE) { 17906 node2 = marker.nextSibling; 17907 if (node2 && node2.nodeType == 3) { 17908 node.appendData(node2.data); 17909 node2.parentNode.removeChild(node2); 17910 } 17911 } 17912 } else { 17913 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 17914 rng.setStartBefore(marker); 17915 rng.setEndBefore(marker); 17916 } 17917 17918 // Remove the marker node and set the new range 17919 dom.remove(marker); 17920 selection.setRng(rng); 17921 17922 // Dispatch after event and add any visual elements needed 17923 editor.fire('SetContent', args); 17924 editor.addVisual(); 17925 }, 17926 17927 mceInsertRawHTML: function(command, ui, value) { 17928 selection.setContent('tiny_mce_marker'); 17929 editor.setContent( 17930 editor.getContent().replace(/tiny_mce_marker/g, function() { 17931 return value; 17932 }) 17933 ); 17934 }, 17935 17936 mceToggleFormat: function(command, ui, value) { 17937 toggleFormat(value); 17938 }, 17939 17940 mceSetContent: function(command, ui, value) { 17941 editor.setContent(value); 17942 }, 17943 17944 'Indent,Outdent': function(command) { 17945 var intentValue, indentUnit, value; 17946 17947 // Setup indent level 17948 intentValue = settings.indentation; 17949 indentUnit = /[a-z%]+$/i.exec(intentValue); 17950 intentValue = parseInt(intentValue, 10); 17951 17952 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { 17953 // If forced_root_blocks is set to false we don't have a block to indent so lets create a div 17954 if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { 17955 formatter.apply('div'); 17956 } 17957 17958 each(selection.getSelectedBlocks(), function(element) { 17959 if (element.nodeName != "LI") { 17960 var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; 17961 17962 indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; 17963 17964 if (command == 'outdent') { 17965 value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); 17966 dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); 17967 } else { 17968 value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; 17969 dom.setStyle(element, indentStyleName, value); 17970 } 17971 } 17972 }); 17973 } else { 17974 execNativeCommand(command); 17975 } 17976 }, 17977 17978 mceRepaint: function() { 17979 if (isGecko) { 17980 try { 17981 storeSelection(TRUE); 17982 17983 if (selection.getSel()) { 17984 selection.getSel().selectAllChildren(editor.getBody()); 17985 } 17986 17987 selection.collapse(TRUE); 17988 restoreSelection(); 17989 } catch (ex) { 17990 // Ignore 17991 } 17992 } 17993 }, 17994 17995 InsertHorizontalRule: function() { 17996 editor.execCommand('mceInsertContent', false, '<hr />'); 17997 }, 17998 17999 mceToggleVisualAid: function() { 18000 editor.hasVisual = !editor.hasVisual; 18001 editor.addVisual(); 18002 }, 18003 18004 mceReplaceContent: function(command, ui, value) { 18005 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'}))); 18006 }, 18007 18008 mceInsertLink: function(command, ui, value) { 18009 var anchor; 18010 18011 if (typeof(value) == 'string') { 18012 value = {href: value}; 18013 } 18014 18015 anchor = dom.getParent(selection.getNode(), 'a'); 18016 18017 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 18018 value.href = value.href.replace(' ', '%20'); 18019 18020 // Remove existing links if there could be child links or that the href isn't specified 18021 if (!anchor || !value.href) { 18022 formatter.remove('link'); 18023 } 18024 18025 // Apply new link to selection 18026 if (value.href) { 18027 formatter.apply('link', value, anchor); 18028 } 18029 }, 18030 18031 selectAll: function() { 18032 var root = dom.getRoot(), rng; 18033 18034 if (selection.getRng().setStart) { 18035 rng = dom.createRng(); 18036 rng.setStart(root, 0); 18037 rng.setEnd(root, root.childNodes.length); 18038 selection.setRng(rng); 18039 } else { 18040 // IE will render it's own root level block elements and sometimes 18041 // even put font elements in them when the user starts typing. So we need to 18042 // move the selection to a more suitable element from this: 18043 // <body>|<p></p></body> to this: <body><p>|</p></body> 18044 rng = selection.getRng(); 18045 if (!rng.item) { 18046 rng.moveToElementText(root); 18047 rng.select(); 18048 } 18049 } 18050 }, 18051 18052 "delete": function() { 18053 execNativeCommand("Delete"); 18054 18055 // Check if body is empty after the delete call if so then set the contents 18056 // to an empty string and move the caret to any block produced by that operation 18057 // this fixes the issue with root blocks not being properly produced after a delete call on IE 18058 var body = editor.getBody(); 18059 18060 if (dom.isEmpty(body)) { 18061 editor.setContent(''); 18062 18063 if (body.firstChild && dom.isBlock(body.firstChild)) { 18064 editor.selection.setCursorLocation(body.firstChild, 0); 18065 } else { 18066 editor.selection.setCursorLocation(body, 0); 18067 } 18068 } 18069 }, 18070 18071 mceNewDocument: function() { 18072 editor.setContent(''); 18073 }, 18074 18075 InsertLineBreak: function(command, ui, value) { 18076 // We load the current event in from EnterKey.js when appropriate to heed 18077 // certain event-specific variations such as ctrl-enter in a list 18078 var evt = value; 18079 var brElm, extraBr, marker; 18080 var rng = selection.getRng(true); 18081 new RangeUtils(dom).normalize(rng); 18082 18083 var offset = rng.startOffset; 18084 var container = rng.startContainer; 18085 18086 // Resolve node index 18087 if (container.nodeType == 1 && container.hasChildNodes()) { 18088 var isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 18089 18090 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 18091 if (isAfterLastNodeInContainer && container.nodeType == 3) { 18092 offset = container.nodeValue.length; 18093 } else { 18094 offset = 0; 18095 } 18096 } 18097 18098 var parentBlock = dom.getParent(container, dom.isBlock); 18099 var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 18100 var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 18101 var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 18102 18103 // Enter inside block contained within a LI then split or insert before/after LI 18104 var isControlKey = evt && evt.ctrlKey; 18105 if (containerBlockName == 'LI' && !isControlKey) { 18106 parentBlock = containerBlock; 18107 parentBlockName = containerBlockName; 18108 } 18109 18110 // Walks the parent block to the right and look for BR elements 18111 function hasRightSideContent() { 18112 var walker = new TreeWalker(container, parentBlock), node; 18113 var nonEmptyElementsMap = editor.schema.getNonEmptyElements(); 18114 18115 while ((node = walker.next())) { 18116 if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { 18117 return true; 18118 } 18119 } 18120 } 18121 18122 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { 18123 // Insert extra BR element at the end block elements 18124 if (!isOldIE && !hasRightSideContent()) { 18125 brElm = dom.create('br'); 18126 rng.insertNode(brElm); 18127 rng.setStartAfter(brElm); 18128 rng.setEndAfter(brElm); 18129 extraBr = true; 18130 } 18131 } 18132 18133 brElm = dom.create('br'); 18134 rng.insertNode(brElm); 18135 18136 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it 18137 var documentMode = dom.doc.documentMode; 18138 if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { 18139 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); 18140 } 18141 18142 // Insert temp marker and scroll to that 18143 marker = dom.create('span', {}, ' '); 18144 brElm.parentNode.insertBefore(marker, brElm); 18145 selection.scrollIntoView(marker); 18146 dom.remove(marker); 18147 18148 if (!extraBr) { 18149 rng.setStartAfter(brElm); 18150 rng.setEndAfter(brElm); 18151 } else { 18152 rng.setStartBefore(brElm); 18153 rng.setEndBefore(brElm); 18154 } 18155 18156 selection.setRng(rng); 18157 editor.undoManager.add(); 18158 18159 return TRUE; 18160 } 18161 }); 18162 18163 // Add queryCommandState overrides 18164 addCommands({ 18165 // Override justify commands 18166 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { 18167 var name = 'align' + command.substring(7); 18168 var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); 18169 var matches = map(nodes, function(node) { 18170 return !!formatter.matchNode(node, name); 18171 }); 18172 return inArray(matches, TRUE) !== -1; 18173 }, 18174 18175 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { 18176 return isFormatMatch(command); 18177 }, 18178 18179 mceBlockQuote: function() { 18180 return isFormatMatch('blockquote'); 18181 }, 18182 18183 Outdent: function() { 18184 var node; 18185 18186 if (settings.inline_styles) { 18187 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { 18188 return TRUE; 18189 } 18190 18191 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { 18192 return TRUE; 18193 } 18194 } 18195 18196 return ( 18197 queryCommandState('InsertUnorderedList') || 18198 queryCommandState('InsertOrderedList') || 18199 (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) 18200 ); 18201 }, 18202 18203 'InsertUnorderedList,InsertOrderedList': function(command) { 18204 var list = dom.getParent(selection.getNode(), 'ul,ol'); 18205 18206 return list && 18207 ( 18208 command === 'insertunorderedlist' && list.tagName === 'UL' || 18209 command === 'insertorderedlist' && list.tagName === 'OL' 18210 ); 18211 } 18212 }, 'state'); 18213 18214 // Add queryCommandValue overrides 18215 addCommands({ 18216 'FontSize,FontName': function(command) { 18217 var value = 0, parent; 18218 18219 if ((parent = dom.getParent(selection.getNode(), 'span'))) { 18220 if (command == 'fontsize') { 18221 value = parent.style.fontSize; 18222 } else { 18223 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); 18224 } 18225 } 18226 18227 return value; 18228 } 18229 }, 'value'); 18230 18231 // Add undo manager logic 18232 addCommands({ 18233 Undo: function() { 18234 editor.undoManager.undo(); 18235 }, 18236 18237 Redo: function() { 18238 editor.undoManager.redo(); 18239 } 18240 }); 18241 }; 18242 }); 18243 18244 // Included from: js/tinymce/classes/util/URI.js 18245 18246 /** 18247 * URI.js 18248 * 18249 * Copyright, Moxiecode Systems AB 18250 * Released under LGPL License. 18251 * 18252 * License: http://www.tinymce.com/license 18253 * Contributing: http://www.tinymce.com/contributing 18254 */ 18255 18256 /** 18257 * This class handles parsing, modification and serialization of URI/URL strings. 18258 * @class tinymce.util.URI 18259 */ 18260 define("tinymce/util/URI", [ 18261 "tinymce/util/Tools" 18262 ], function(Tools) { 18263 var each = Tools.each, trim = Tools.trim; 18264 var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' '); 18265 var DEFAULT_PORTS = { 18266 'ftp': 21, 18267 'http': 80, 18268 'https': 443, 18269 'mailto': 25 18270 }; 18271 18272 /** 18273 * Constructs a new URI instance. 18274 * 18275 * @constructor 18276 * @method URI 18277 * @param {String} url URI string to parse. 18278 * @param {Object} settings Optional settings object. 18279 */ 18280 function URI(url, settings) { 18281 var self = this, baseUri, base_url; 18282 18283 url = trim(url); 18284 settings = self.settings = settings || {}; 18285 baseUri = settings.base_uri; 18286 18287 // Strange app protocol that isn't http/https or local anchor 18288 // For example: mailto,skype,tel etc. 18289 if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { 18290 self.source = url; 18291 return; 18292 } 18293 18294 var isProtocolRelative = url.indexOf('//') === 0; 18295 18296 // Absolute path with no host, fake host and protocol 18297 if (url.indexOf('/') === 0 && !isProtocolRelative) { 18298 url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; 18299 } 18300 18301 // Relative path http:// or protocol relative //path 18302 if (!/^[\w\-]*:?\/\//.test(url)) { 18303 base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory; 18304 if (settings.base_uri.protocol === "") { 18305 url = '//mce_host' + self.toAbsPath(base_url, url); 18306 } else { 18307 url = /([^#?]*)([#?]?.*)/.exec(url); 18308 url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url[1]) + url[2]; 18309 } 18310 } 18311 18312 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 18313 url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 18314 18315 /*jshint maxlen: 255 */ 18316 /*eslint max-len: 0 */ 18317 url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); 18318 18319 each(queryParts, function(v, i) { 18320 var part = url[i]; 18321 18322 // Zope 3 workaround, they use @@something 18323 if (part) { 18324 part = part.replace(/\(mce_at\)/g, '@@'); 18325 } 18326 18327 self[v] = part; 18328 }); 18329 18330 if (baseUri) { 18331 if (!self.protocol) { 18332 self.protocol = baseUri.protocol; 18333 } 18334 18335 if (!self.userInfo) { 18336 self.userInfo = baseUri.userInfo; 18337 } 18338 18339 if (!self.port && self.host === 'mce_host') { 18340 self.port = baseUri.port; 18341 } 18342 18343 if (!self.host || self.host === 'mce_host') { 18344 self.host = baseUri.host; 18345 } 18346 18347 self.source = ''; 18348 } 18349 18350 if (isProtocolRelative) { 18351 self.protocol = ''; 18352 } 18353 18354 //t.path = t.path || '/'; 18355 } 18356 18357 URI.prototype = { 18358 /** 18359 * Sets the internal path part of the URI. 18360 * 18361 * @method setPath 18362 * @param {string} path Path string to set. 18363 */ 18364 setPath: function(path) { 18365 var self = this; 18366 18367 path = /^(.*?)\/?(\w+)?$/.exec(path); 18368 18369 // Update path parts 18370 self.path = path[0]; 18371 self.directory = path[1]; 18372 self.file = path[2]; 18373 18374 // Rebuild source 18375 self.source = ''; 18376 self.getURI(); 18377 }, 18378 18379 /** 18380 * Converts the specified URI into a relative URI based on the current URI instance location. 18381 * 18382 * @method toRelative 18383 * @param {String} uri URI to convert into a relative path/URI. 18384 * @return {String} Relative URI from the point specified in the current URI instance. 18385 * @example 18386 * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm 18387 * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); 18388 */ 18389 toRelative: function(uri) { 18390 var self = this, output; 18391 18392 if (uri === "./") { 18393 return uri; 18394 } 18395 18396 uri = new URI(uri, {base_uri: self}); 18397 18398 // Not on same domain/port or protocol 18399 if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || 18400 (self.protocol != uri.protocol && uri.protocol !== "")) { 18401 return uri.getURI(); 18402 } 18403 18404 var tu = self.getURI(), uu = uri.getURI(); 18405 18406 // Allow usage of the base_uri when relative_urls = true 18407 if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { 18408 return tu; 18409 } 18410 18411 output = self.toRelPath(self.path, uri.path); 18412 18413 // Add query 18414 if (uri.query) { 18415 output += '?' + uri.query; 18416 } 18417 18418 // Add anchor 18419 if (uri.anchor) { 18420 output += '#' + uri.anchor; 18421 } 18422 18423 return output; 18424 }, 18425 18426 /** 18427 * Converts the specified URI into a absolute URI based on the current URI instance location. 18428 * 18429 * @method toAbsolute 18430 * @param {String} uri URI to convert into a relative path/URI. 18431 * @param {Boolean} noHost No host and protocol prefix. 18432 * @return {String} Absolute URI from the point specified in the current URI instance. 18433 * @example 18434 * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm 18435 * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); 18436 */ 18437 toAbsolute: function(uri, noHost) { 18438 uri = new URI(uri, {base_uri: this}); 18439 18440 return uri.getURI(noHost && this.isSameOrigin(uri)); 18441 }, 18442 18443 /** 18444 * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. 18445 * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they 18446 * won't match, if the port specifications differ. 18447 * 18448 * @method isSameOrigin 18449 * @param {tinymce.util.URI} uri Uri instance to compare. 18450 * @returns {Boolean} True if the origins are the same. 18451 */ 18452 isSameOrigin: function(uri) { 18453 if (this.host == uri.host && this.protocol == uri.protocol) { 18454 if (this.port == uri.port) { 18455 return true; 18456 } 18457 18458 var defaultPort = DEFAULT_PORTS[this.protocol]; 18459 if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { 18460 return true; 18461 } 18462 } 18463 18464 return false; 18465 }, 18466 18467 /** 18468 * Converts a absolute path into a relative path. 18469 * 18470 * @method toRelPath 18471 * @param {String} base Base point to convert the path from. 18472 * @param {String} path Absolute path to convert into a relative path. 18473 */ 18474 toRelPath: function(base, path) { 18475 var items, breakPoint = 0, out = '', i, l; 18476 18477 // Split the paths 18478 base = base.substring(0, base.lastIndexOf('/')); 18479 base = base.split('/'); 18480 items = path.split('/'); 18481 18482 if (base.length >= items.length) { 18483 for (i = 0, l = base.length; i < l; i++) { 18484 if (i >= items.length || base[i] != items[i]) { 18485 breakPoint = i + 1; 18486 break; 18487 } 18488 } 18489 } 18490 18491 if (base.length < items.length) { 18492 for (i = 0, l = items.length; i < l; i++) { 18493 if (i >= base.length || base[i] != items[i]) { 18494 breakPoint = i + 1; 18495 break; 18496 } 18497 } 18498 } 18499 18500 if (breakPoint === 1) { 18501 return path; 18502 } 18503 18504 for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { 18505 out += "../"; 18506 } 18507 18508 for (i = breakPoint - 1, l = items.length; i < l; i++) { 18509 if (i != breakPoint - 1) { 18510 out += "/" + items[i]; 18511 } else { 18512 out += items[i]; 18513 } 18514 } 18515 18516 return out; 18517 }, 18518 18519 /** 18520 * Converts a relative path into a absolute path. 18521 * 18522 * @method toAbsPath 18523 * @param {String} base Base point to convert the path from. 18524 * @param {String} path Relative path to convert into an absolute path. 18525 */ 18526 toAbsPath: function(base, path) { 18527 var i, nb = 0, o = [], tr, outPath; 18528 18529 // Split paths 18530 tr = /\/$/.test(path) ? '/' : ''; 18531 base = base.split('/'); 18532 path = path.split('/'); 18533 18534 // Remove empty chunks 18535 each(base, function(k) { 18536 if (k) { 18537 o.push(k); 18538 } 18539 }); 18540 18541 base = o; 18542 18543 // Merge relURLParts chunks 18544 for (i = path.length - 1, o = []; i >= 0; i--) { 18545 // Ignore empty or . 18546 if (path[i].length === 0 || path[i] === ".") { 18547 continue; 18548 } 18549 18550 // Is parent 18551 if (path[i] === '..') { 18552 nb++; 18553 continue; 18554 } 18555 18556 // Move up 18557 if (nb > 0) { 18558 nb--; 18559 continue; 18560 } 18561 18562 o.push(path[i]); 18563 } 18564 18565 i = base.length - nb; 18566 18567 // If /a/b/c or / 18568 if (i <= 0) { 18569 outPath = o.reverse().join('/'); 18570 } else { 18571 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); 18572 } 18573 18574 // Add front / if it's needed 18575 if (outPath.indexOf('/') !== 0) { 18576 outPath = '/' + outPath; 18577 } 18578 18579 // Add traling / if it's needed 18580 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { 18581 outPath += tr; 18582 } 18583 18584 return outPath; 18585 }, 18586 18587 /** 18588 * Returns the full URI of the internal structure. 18589 * 18590 * @method getURI 18591 * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. 18592 */ 18593 getURI: function(noProtoHost) { 18594 var s, self = this; 18595 18596 // Rebuild source 18597 if (!self.source || noProtoHost) { 18598 s = ''; 18599 18600 if (!noProtoHost) { 18601 if (self.protocol) { 18602 s += self.protocol + '://'; 18603 } else { 18604 s += '//'; 18605 } 18606 18607 if (self.userInfo) { 18608 s += self.userInfo + '@'; 18609 } 18610 18611 if (self.host) { 18612 s += self.host; 18613 } 18614 18615 if (self.port) { 18616 s += ':' + self.port; 18617 } 18618 } 18619 18620 if (self.path) { 18621 s += self.path; 18622 } 18623 18624 if (self.query) { 18625 s += '?' + self.query; 18626 } 18627 18628 if (self.anchor) { 18629 s += '#' + self.anchor; 18630 } 18631 18632 self.source = s; 18633 } 18634 18635 return self.source; 18636 } 18637 }; 18638 18639 return URI; 18640 }); 18641 18642 // Included from: js/tinymce/classes/util/Class.js 18643 18644 /** 18645 * Class.js 18646 * 18647 * Copyright 2003-2012, Moxiecode Systems AB, All rights reserved. 18648 */ 18649 18650 /** 18651 * This utilitiy class is used for easier inheritage. 18652 * 18653 * Features: 18654 * * Exposed super functions: this._super(); 18655 * * Mixins 18656 * * Dummy functions 18657 * * Property functions: var value = object.value(); and object.value(newValue); 18658 * * Static functions 18659 * * Defaults settings 18660 */ 18661 define("tinymce/util/Class", [ 18662 "tinymce/util/Tools" 18663 ], function(Tools) { 18664 var each = Tools.each, extend = Tools.extend; 18665 18666 var extendClass, initializing; 18667 18668 function Class() { 18669 } 18670 18671 // Provides classical inheritance, based on code made by John Resig 18672 Class.extend = extendClass = function(prop) { 18673 var self = this, _super = self.prototype, prototype, name, member; 18674 18675 // The dummy class constructor 18676 function Class() { 18677 var i, mixins, mixin, self = this; 18678 18679 // All construction is actually done in the init method 18680 if (!initializing) { 18681 // Run class constuctor 18682 if (self.init) { 18683 self.init.apply(self, arguments); 18684 } 18685 18686 // Run mixin constructors 18687 mixins = self.Mixins; 18688 if (mixins) { 18689 i = mixins.length; 18690 while (i--) { 18691 mixin = mixins[i]; 18692 if (mixin.init) { 18693 mixin.init.apply(self, arguments); 18694 } 18695 } 18696 } 18697 } 18698 } 18699 18700 // Dummy function, needs to be extended in order to provide functionality 18701 function dummy() { 18702 return this; 18703 } 18704 18705 // Creates a overloaded method for the class 18706 // this enables you to use this._super(); to call the super function 18707 function createMethod(name, fn) { 18708 return function() { 18709 var self = this, tmp = self._super, ret; 18710 18711 self._super = _super[name]; 18712 ret = fn.apply(self, arguments); 18713 self._super = tmp; 18714 18715 return ret; 18716 }; 18717 } 18718 18719 // Instantiate a base class (but only create the instance, 18720 // don't run the init constructor) 18721 initializing = true; 18722 18723 /*eslint new-cap:0 */ 18724 prototype = new self(); 18725 initializing = false; 18726 18727 // Add mixins 18728 if (prop.Mixins) { 18729 each(prop.Mixins, function(mixin) { 18730 mixin = mixin; 18731 18732 for (var name in mixin) { 18733 if (name !== "init") { 18734 prop[name] = mixin[name]; 18735 } 18736 } 18737 }); 18738 18739 if (_super.Mixins) { 18740 prop.Mixins = _super.Mixins.concat(prop.Mixins); 18741 } 18742 } 18743 18744 // Generate dummy methods 18745 if (prop.Methods) { 18746 each(prop.Methods.split(','), function(name) { 18747 prop[name] = dummy; 18748 }); 18749 } 18750 18751 // Generate property methods 18752 if (prop.Properties) { 18753 each(prop.Properties.split(','), function(name) { 18754 var fieldName = '_' + name; 18755 18756 prop[name] = function(value) { 18757 var self = this, undef; 18758 18759 // Set value 18760 if (value !== undef) { 18761 self[fieldName] = value; 18762 18763 return self; 18764 } 18765 18766 // Get value 18767 return self[fieldName]; 18768 }; 18769 }); 18770 } 18771 18772 // Static functions 18773 if (prop.Statics) { 18774 each(prop.Statics, function(func, name) { 18775 Class[name] = func; 18776 }); 18777 } 18778 18779 // Default settings 18780 if (prop.Defaults && _super.Defaults) { 18781 prop.Defaults = extend({}, _super.Defaults, prop.Defaults); 18782 } 18783 18784 // Copy the properties over onto the new prototype 18785 for (name in prop) { 18786 member = prop[name]; 18787 18788 if (typeof member == "function" && _super[name]) { 18789 prototype[name] = createMethod(name, member); 18790 } else { 18791 prototype[name] = member; 18792 } 18793 } 18794 18795 // Populate our constructed prototype object 18796 Class.prototype = prototype; 18797 18798 // Enforce the constructor to be what we expect 18799 Class.constructor = Class; 18800 18801 // And make this class extendible 18802 Class.extend = extendClass; 18803 18804 return Class; 18805 }; 18806 18807 return Class; 18808 }); 18809 18810 // Included from: js/tinymce/classes/util/EventDispatcher.js 18811 18812 /** 18813 * EventDispatcher.js 18814 * 18815 * Copyright, Moxiecode Systems AB 18816 * Released under LGPL License. 18817 * 18818 * License: http://www.tinymce.com/license 18819 * Contributing: http://www.tinymce.com/contributing 18820 */ 18821 18822 /** 18823 * This class lets you add/remove and fire events by name on the specified scope. This makes 18824 * it easy to add event listener logic to any class. 18825 * 18826 * @class tinymce.util.EventDispatcher 18827 * @example 18828 * var eventDispatcher = new EventDispatcher(); 18829 * 18830 * eventDispatcher.on('click', function() {console.log('data');}); 18831 * eventDispatcher.fire('click', {data: 123}); 18832 */ 18833 define("tinymce/util/EventDispatcher", [ 18834 "tinymce/util/Tools" 18835 ], function(Tools) { 18836 var nativeEvents = Tools.makeMap( 18837 "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + 18838 "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + 18839 "draggesture dragdrop drop drag submit " + 18840 "compositionstart compositionend compositionupdate touchstart touchend", 18841 ' ' 18842 ); 18843 18844 function Dispatcher(settings) { 18845 var self = this, scope, bindings = {}, toggleEvent; 18846 18847 function returnFalse() { 18848 return false; 18849 } 18850 18851 function returnTrue() { 18852 return true; 18853 } 18854 18855 settings = settings || {}; 18856 scope = settings.scope || self; 18857 toggleEvent = settings.toggleEvent || returnFalse; 18858 18859 /** 18860 * Fires the specified event by name. 18861 * 18862 * @method fire 18863 * @param {String} name Name of the event to fire. 18864 * @param {Object?} args Event arguments. 18865 * @return {Object} Event args instance passed in. 18866 * @example 18867 * instance.fire('event', {...}); 18868 */ 18869 function fire(name, args) { 18870 var handlers, i, l, callback; 18871 18872 name = name.toLowerCase(); 18873 args = args || {}; 18874 args.type = name; 18875 18876 // Setup target is there isn't one 18877 if (!args.target) { 18878 args.target = scope; 18879 } 18880 18881 // Add event delegation methods if they are missing 18882 if (!args.preventDefault) { 18883 // Add preventDefault method 18884 args.preventDefault = function() { 18885 args.isDefaultPrevented = returnTrue; 18886 }; 18887 18888 // Add stopPropagation 18889 args.stopPropagation = function() { 18890 args.isPropagationStopped = returnTrue; 18891 }; 18892 18893 // Add stopImmediatePropagation 18894 args.stopImmediatePropagation = function() { 18895 args.isImmediatePropagationStopped = returnTrue; 18896 }; 18897 18898 // Add event delegation states 18899 args.isDefaultPrevented = returnFalse; 18900 args.isPropagationStopped = returnFalse; 18901 args.isImmediatePropagationStopped = returnFalse; 18902 } 18903 18904 if (settings.beforeFire) { 18905 settings.beforeFire(args); 18906 } 18907 18908 handlers = bindings[name]; 18909 if (handlers) { 18910 for (i = 0, l = handlers.length; i < l; i++) { 18911 handlers[i] = callback = handlers[i]; 18912 18913 // Unbind handlers marked with "once" 18914 if (callback.once) { 18915 off(name, callback); 18916 } 18917 18918 // Stop immediate propagation if needed 18919 if (args.isImmediatePropagationStopped()) { 18920 args.stopPropagation(); 18921 return args; 18922 } 18923 18924 // If callback returns false then prevent default and stop all propagation 18925 if (callback.call(scope, args) === false) { 18926 args.preventDefault(); 18927 return args; 18928 } 18929 } 18930 } 18931 18932 return args; 18933 } 18934 18935 /** 18936 * Binds an event listener to a specific event by name. 18937 * 18938 * @method on 18939 * @param {String} name Event name or space separated list of events to bind. 18940 * @param {callback} callback Callback to be executed when the event occurs. 18941 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 18942 * @return {Object} Current class instance. 18943 * @example 18944 * instance.on('event', function(e) { 18945 * // Callback logic 18946 * }); 18947 */ 18948 function on(name, callback, prepend) { 18949 var handlers, names, i; 18950 18951 if (callback === false) { 18952 callback = returnFalse; 18953 } 18954 18955 if (callback) { 18956 names = name.toLowerCase().split(' '); 18957 i = names.length; 18958 while (i--) { 18959 name = names[i]; 18960 handlers = bindings[name]; 18961 if (!handlers) { 18962 handlers = bindings[name] = []; 18963 toggleEvent(name, true); 18964 } 18965 18966 if (prepend) { 18967 handlers.unshift(callback); 18968 } else { 18969 handlers.push(callback); 18970 } 18971 } 18972 } 18973 18974 return self; 18975 } 18976 18977 /** 18978 * Unbinds an event listener to a specific event by name. 18979 * 18980 * @method off 18981 * @param {String?} name Name of the event to unbind. 18982 * @param {callback?} callback Callback to unbind. 18983 * @return {Object} Current class instance. 18984 * @example 18985 * // Unbind specific callback 18986 * instance.off('event', handler); 18987 * 18988 * // Unbind all listeners by name 18989 * instance.off('event'); 18990 * 18991 * // Unbind all events 18992 * instance.off(); 18993 */ 18994 function off(name, callback) { 18995 var i, handlers, bindingName, names, hi; 18996 18997 if (name) { 18998 names = name.toLowerCase().split(' '); 18999 i = names.length; 19000 while (i--) { 19001 name = names[i]; 19002 handlers = bindings[name]; 19003 19004 // Unbind all handlers 19005 if (!name) { 19006 for (bindingName in bindings) { 19007 toggleEvent(bindingName, false); 19008 delete bindings[bindingName]; 19009 } 19010 19011 return self; 19012 } 19013 19014 if (handlers) { 19015 // Unbind all by name 19016 if (!callback) { 19017 handlers.length = 0; 19018 } else { 19019 // Unbind specific ones 19020 hi = handlers.length; 19021 while (hi--) { 19022 if (handlers[hi] === callback) { 19023 handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1)); 19024 bindings[name] = handlers; 19025 } 19026 } 19027 } 19028 19029 if (!handlers.length) { 19030 toggleEvent(name, false); 19031 delete bindings[name]; 19032 } 19033 } 19034 } 19035 } else { 19036 for (name in bindings) { 19037 toggleEvent(name, false); 19038 } 19039 19040 bindings = {}; 19041 } 19042 19043 return self; 19044 } 19045 19046 /** 19047 * Binds an event listener to a specific event by name 19048 * and automatically unbind the event once the callback fires. 19049 * 19050 * @method once 19051 * @param {String} name Event name or space separated list of events to bind. 19052 * @param {callback} callback Callback to be executed when the event occurs. 19053 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 19054 * @return {Object} Current class instance. 19055 * @example 19056 * instance.once('event', function(e) { 19057 * // Callback logic 19058 * }); 19059 */ 19060 function once(name, callback, prepend) { 19061 callback.once = true; 19062 return on(name, callback, prepend); 19063 } 19064 19065 /** 19066 * Returns true/false if the dispatcher has a event of the specified name. 19067 * 19068 * @method has 19069 * @param {String} name Name of the event to check for. 19070 * @return {Boolean} true/false if the event exists or not. 19071 */ 19072 function has(name) { 19073 name = name.toLowerCase(); 19074 return !(!bindings[name] || bindings[name].length === 0); 19075 } 19076 19077 // Expose 19078 self.fire = fire; 19079 self.on = on; 19080 self.off = off; 19081 self.once = once; 19082 self.has = has; 19083 } 19084 19085 /** 19086 * Returns true/false if the specified event name is a native browser event or not. 19087 * 19088 * @method isNative 19089 * @param {String} name Name to check if it's native. 19090 * @return {Boolean} true/false if the event is native or not. 19091 * @static 19092 */ 19093 Dispatcher.isNative = function(name) { 19094 return !!nativeEvents[name.toLowerCase()]; 19095 }; 19096 19097 return Dispatcher; 19098 }); 19099 19100 // Included from: js/tinymce/classes/ui/Selector.js 19101 19102 /** 19103 * Selector.js 19104 * 19105 * Copyright, Moxiecode Systems AB 19106 * Released under LGPL License. 19107 * 19108 * License: http://www.tinymce.com/license 19109 * Contributing: http://www.tinymce.com/contributing 19110 */ 19111 19112 /*eslint no-nested-ternary:0 */ 19113 19114 /** 19115 * Selector engine, enables you to select controls by using CSS like expressions. 19116 * We currently only support basic CSS expressions to reduce the size of the core 19117 * and the ones we support should be enough for most cases. 19118 * 19119 * @example 19120 * Supported expressions: 19121 * element 19122 * element#name 19123 * element.class 19124 * element[attr] 19125 * element[attr*=value] 19126 * element[attr~=value] 19127 * element[attr!=value] 19128 * element[attr^=value] 19129 * element[attr$=value] 19130 * element:<state> 19131 * element:not(<expression>) 19132 * element:first 19133 * element:last 19134 * element:odd 19135 * element:even 19136 * element element 19137 * element > element 19138 * 19139 * @class tinymce.ui.Selector 19140 */ 19141 define("tinymce/ui/Selector", [ 19142 "tinymce/util/Class" 19143 ], function(Class) { 19144 "use strict"; 19145 19146 /** 19147 * Produces an array with a unique set of objects. It will not compare the values 19148 * but the references of the objects. 19149 * 19150 * @private 19151 * @method unqiue 19152 * @param {Array} array Array to make into an array with unique items. 19153 * @return {Array} Array with unique items. 19154 */ 19155 function unique(array) { 19156 var uniqueItems = [], i = array.length, item; 19157 19158 while (i--) { 19159 item = array[i]; 19160 19161 if (!item.__checked) { 19162 uniqueItems.push(item); 19163 item.__checked = 1; 19164 } 19165 } 19166 19167 i = uniqueItems.length; 19168 while (i--) { 19169 delete uniqueItems[i].__checked; 19170 } 19171 19172 return uniqueItems; 19173 } 19174 19175 var expression = /^([\w\\*]+)?(?:#([\w\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i; 19176 19177 /*jshint maxlen:255 */ 19178 /*eslint max-len:0 */ 19179 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, 19180 whiteSpace = /^\s*|\s*$/g, 19181 Collection; 19182 19183 var Selector = Class.extend({ 19184 /** 19185 * Constructs a new Selector instance. 19186 * 19187 * @constructor 19188 * @method init 19189 * @param {String} selector CSS like selector expression. 19190 */ 19191 init: function(selector) { 19192 var match = this.match; 19193 19194 function compileNameFilter(name) { 19195 if (name) { 19196 name = name.toLowerCase(); 19197 19198 return function(item) { 19199 return name === '*' || item.type === name; 19200 }; 19201 } 19202 } 19203 19204 function compileIdFilter(id) { 19205 if (id) { 19206 return function(item) { 19207 return item._name === id; 19208 }; 19209 } 19210 } 19211 19212 function compileClassesFilter(classes) { 19213 if (classes) { 19214 classes = classes.split('.'); 19215 19216 return function(item) { 19217 var i = classes.length; 19218 19219 while (i--) { 19220 if (!item.hasClass(classes[i])) { 19221 return false; 19222 } 19223 } 19224 19225 return true; 19226 }; 19227 } 19228 } 19229 19230 function compileAttrFilter(name, cmp, check) { 19231 if (name) { 19232 return function(item) { 19233 var value = item[name] ? item[name]() : ''; 19234 19235 return !cmp ? !!check : 19236 cmp === "=" ? value === check : 19237 cmp === "*=" ? value.indexOf(check) >= 0 : 19238 cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 : 19239 cmp === "!=" ? value != check : 19240 cmp === "^=" ? value.indexOf(check) === 0 : 19241 cmp === "$=" ? value.substr(value.length - check.length) === check : 19242 false; 19243 }; 19244 } 19245 } 19246 19247 function compilePsuedoFilter(name) { 19248 var notSelectors; 19249 19250 if (name) { 19251 name = /(?:not\((.+)\))|(.+)/i.exec(name); 19252 19253 if (!name[1]) { 19254 name = name[2]; 19255 19256 return function(item, index, length) { 19257 return name === 'first' ? index === 0 : 19258 name === 'last' ? index === length - 1 : 19259 name === 'even' ? index % 2 === 0 : 19260 name === 'odd' ? index % 2 === 1 : 19261 item[name] ? item[name]() : 19262 false; 19263 }; 19264 } else { 19265 // Compile not expression 19266 notSelectors = parseChunks(name[1], []); 19267 19268 return function(item) { 19269 return !match(item, notSelectors); 19270 }; 19271 } 19272 } 19273 } 19274 19275 function compile(selector, filters, direct) { 19276 var parts; 19277 19278 function add(filter) { 19279 if (filter) { 19280 filters.push(filter); 19281 } 19282 } 19283 19284 // Parse expression into parts 19285 parts = expression.exec(selector.replace(whiteSpace, '')); 19286 19287 add(compileNameFilter(parts[1])); 19288 add(compileIdFilter(parts[2])); 19289 add(compileClassesFilter(parts[3])); 19290 add(compileAttrFilter(parts[4], parts[5], parts[6])); 19291 add(compilePsuedoFilter(parts[7])); 19292 19293 // Mark the filter with psuedo for performance 19294 filters.psuedo = !!parts[7]; 19295 filters.direct = direct; 19296 19297 return filters; 19298 } 19299 19300 // Parser logic based on Sizzle by John Resig 19301 function parseChunks(selector, selectors) { 19302 var parts = [], extra, matches, i; 19303 19304 do { 19305 chunker.exec(""); 19306 matches = chunker.exec(selector); 19307 19308 if (matches) { 19309 selector = matches[3]; 19310 parts.push(matches[1]); 19311 19312 if (matches[2]) { 19313 extra = matches[3]; 19314 break; 19315 } 19316 } 19317 } while (matches); 19318 19319 if (extra) { 19320 parseChunks(extra, selectors); 19321 } 19322 19323 selector = []; 19324 for (i = 0; i < parts.length; i++) { 19325 if (parts[i] != '>') { 19326 selector.push(compile(parts[i], [], parts[i - 1] === '>')); 19327 } 19328 } 19329 19330 selectors.push(selector); 19331 19332 return selectors; 19333 } 19334 19335 this._selectors = parseChunks(selector, []); 19336 }, 19337 19338 /** 19339 * Returns true/false if the selector matches the specified control. 19340 * 19341 * @method match 19342 * @param {tinymce.ui.Control} control Control to match agains the selector. 19343 * @param {Array} selectors Optional array of selectors, mostly used internally. 19344 * @return {Boolean} true/false state if the control matches or not. 19345 */ 19346 match: function(control, selectors) { 19347 var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item; 19348 19349 selectors = selectors || this._selectors; 19350 for (i = 0, l = selectors.length; i < l; i++) { 19351 selector = selectors[i]; 19352 sl = selector.length; 19353 item = control; 19354 count = 0; 19355 19356 for (si = sl - 1; si >= 0; si--) { 19357 filters = selector[si]; 19358 19359 while (item) { 19360 // Find the index and length since a psuedo filter like :first needs it 19361 if (filters.psuedo) { 19362 siblings = item.parent().items(); 19363 index = length = siblings.length; 19364 while (index--) { 19365 if (siblings[index] === item) { 19366 break; 19367 } 19368 } 19369 } 19370 19371 for (fi = 0, fl = filters.length; fi < fl; fi++) { 19372 if (!filters[fi](item, index, length)) { 19373 fi = fl + 1; 19374 break; 19375 } 19376 } 19377 19378 if (fi === fl) { 19379 count++; 19380 break; 19381 } else { 19382 // If it didn't match the right most expression then 19383 // break since it's no point looking at the parents 19384 if (si === sl - 1) { 19385 break; 19386 } 19387 } 19388 19389 item = item.parent(); 19390 } 19391 } 19392 19393 // If we found all selectors then return true otherwise continue looking 19394 if (count === sl) { 19395 return true; 19396 } 19397 } 19398 19399 return false; 19400 }, 19401 19402 /** 19403 * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container. 19404 * 19405 * @method find 19406 * @param {tinymce.ui.Control} container Container to look for items in. 19407 * @return {tinymce.ui.Collection} Collection with matched elements. 19408 */ 19409 find: function(container) { 19410 var matches = [], i, l, selectors = this._selectors; 19411 19412 function collect(items, selector, index) { 19413 var i, l, fi, fl, item, filters = selector[index]; 19414 19415 for (i = 0, l = items.length; i < l; i++) { 19416 item = items[i]; 19417 19418 // Run each filter agains the item 19419 for (fi = 0, fl = filters.length; fi < fl; fi++) { 19420 if (!filters[fi](item, i, l)) { 19421 fi = fl + 1; 19422 break; 19423 } 19424 } 19425 19426 // All filters matched the item 19427 if (fi === fl) { 19428 // Matched item is on the last expression like: panel toolbar [button] 19429 if (index == selector.length - 1) { 19430 matches.push(item); 19431 } else { 19432 // Collect next expression type 19433 if (item.items) { 19434 collect(item.items(), selector, index + 1); 19435 } 19436 } 19437 } else if (filters.direct) { 19438 return; 19439 } 19440 19441 // Collect child items 19442 if (item.items) { 19443 collect(item.items(), selector, index); 19444 } 19445 } 19446 } 19447 19448 if (container.items) { 19449 for (i = 0, l = selectors.length; i < l; i++) { 19450 collect(container.items(), selectors[i], 0); 19451 } 19452 19453 // Unique the matches if needed 19454 if (l > 1) { 19455 matches = unique(matches); 19456 } 19457 } 19458 19459 // Fix for circular reference 19460 if (!Collection) { 19461 // TODO: Fix me! 19462 Collection = Selector.Collection; 19463 } 19464 19465 return new Collection(matches); 19466 } 19467 }); 19468 19469 return Selector; 19470 }); 19471 19472 // Included from: js/tinymce/classes/ui/Collection.js 19473 19474 /** 19475 * Collection.js 19476 * 19477 * Copyright, Moxiecode Systems AB 19478 * Released under LGPL License. 19479 * 19480 * License: http://www.tinymce.com/license 19481 * Contributing: http://www.tinymce.com/contributing 19482 */ 19483 19484 /** 19485 * Control collection, this class contains control instances and it enables you to 19486 * perform actions on all the contained items. This is very similar to how jQuery works. 19487 * 19488 * @example 19489 * someCollection.show().disabled(true); 19490 * 19491 * @class tinymce.ui.Collection 19492 */ 19493 define("tinymce/ui/Collection", [ 19494 "tinymce/util/Tools", 19495 "tinymce/ui/Selector", 19496 "tinymce/util/Class" 19497 ], function(Tools, Selector, Class) { 19498 "use strict"; 19499 19500 var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice; 19501 19502 proto = { 19503 /** 19504 * Current number of contained control instances. 19505 * 19506 * @field length 19507 * @type Number 19508 */ 19509 length: 0, 19510 19511 /** 19512 * Constructor for the collection. 19513 * 19514 * @constructor 19515 * @method init 19516 * @param {Array} items Optional array with items to add. 19517 */ 19518 init: function(items) { 19519 if (items) { 19520 this.add(items); 19521 } 19522 }, 19523 19524 /** 19525 * Adds new items to the control collection. 19526 * 19527 * @method add 19528 * @param {Array} items Array if items to add to collection. 19529 * @return {tinymce.ui.Collection} Current collection instance. 19530 */ 19531 add: function(items) { 19532 var self = this; 19533 19534 // Force single item into array 19535 if (!Tools.isArray(items)) { 19536 if (items instanceof Collection) { 19537 self.add(items.toArray()); 19538 } else { 19539 push.call(self, items); 19540 } 19541 } else { 19542 push.apply(self, items); 19543 } 19544 19545 return self; 19546 }, 19547 19548 /** 19549 * Sets the contents of the collection. This will remove any existing items 19550 * and replace them with the ones specified in the input array. 19551 * 19552 * @method set 19553 * @param {Array} items Array with items to set into the Collection. 19554 * @return {tinymce.ui.Collection} Collection instance. 19555 */ 19556 set: function(items) { 19557 var self = this, len = self.length, i; 19558 19559 self.length = 0; 19560 self.add(items); 19561 19562 // Remove old entries 19563 for (i = self.length; i < len; i++) { 19564 delete self[i]; 19565 } 19566 19567 return self; 19568 }, 19569 19570 /** 19571 * Filters the collection item based on the specified selector expression or selector function. 19572 * 19573 * @method filter 19574 * @param {String} selector Selector expression to filter items by. 19575 * @return {tinymce.ui.Collection} Collection containing the filtered items. 19576 */ 19577 filter: function(selector) { 19578 var self = this, i, l, matches = [], item, match; 19579 19580 // Compile string into selector expression 19581 if (typeof(selector) === "string") { 19582 selector = new Selector(selector); 19583 19584 match = function(item) { 19585 return selector.match(item); 19586 }; 19587 } else { 19588 // Use selector as matching function 19589 match = selector; 19590 } 19591 19592 for (i = 0, l = self.length; i < l; i++) { 19593 item = self[i]; 19594 19595 if (match(item)) { 19596 matches.push(item); 19597 } 19598 } 19599 19600 return new Collection(matches); 19601 }, 19602 19603 /** 19604 * Slices the items within the collection. 19605 * 19606 * @method slice 19607 * @param {Number} index Index to slice at. 19608 * @param {Number} len Optional length to slice. 19609 * @return {tinymce.ui.Collection} Current collection. 19610 */ 19611 slice: function() { 19612 return new Collection(slice.apply(this, arguments)); 19613 }, 19614 19615 /** 19616 * Makes the current collection equal to the specified index. 19617 * 19618 * @method eq 19619 * @param {Number} index Index of the item to set the collection to. 19620 * @return {tinymce.ui.Collection} Current collection. 19621 */ 19622 eq: function(index) { 19623 return index === -1 ? this.slice(index) : this.slice(index, +index + 1); 19624 }, 19625 19626 /** 19627 * Executes the specified callback on each item in collection. 19628 * 19629 * @method each 19630 * @param {function} callback Callback to execute for each item in collection. 19631 * @return {tinymce.ui.Collection} Current collection instance. 19632 */ 19633 each: function(callback) { 19634 Tools.each(this, callback); 19635 19636 return this; 19637 }, 19638 19639 /** 19640 * Returns an JavaScript array object of the contents inside the collection. 19641 * 19642 * @method toArray 19643 * @return {Array} Array with all items from collection. 19644 */ 19645 toArray: function() { 19646 return Tools.toArray(this); 19647 }, 19648 19649 /** 19650 * Finds the index of the specified control or return -1 if it isn't in the collection. 19651 * 19652 * @method indexOf 19653 * @param {Control} ctrl Control instance to look for. 19654 * @return {Number} Index of the specified control or -1. 19655 */ 19656 indexOf: function(ctrl) { 19657 var self = this, i = self.length; 19658 19659 while (i--) { 19660 if (self[i] === ctrl) { 19661 break; 19662 } 19663 } 19664 19665 return i; 19666 }, 19667 19668 /** 19669 * Returns a new collection of the contents in reverse order. 19670 * 19671 * @method reverse 19672 * @return {tinymce.ui.Collection} Collection instance with reversed items. 19673 */ 19674 reverse: function() { 19675 return new Collection(Tools.toArray(this).reverse()); 19676 }, 19677 19678 /** 19679 * Returns true/false if the class exists or not. 19680 * 19681 * @method hasClass 19682 * @param {String} cls Class to check for. 19683 * @return {Boolean} true/false state if the class exists or not. 19684 */ 19685 hasClass: function(cls) { 19686 return this[0] ? this[0].hasClass(cls) : false; 19687 }, 19688 19689 /** 19690 * Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>); 19691 * 19692 * @method prop 19693 * @param {String} name Property name to get/set. 19694 * @param {Object} value Optional object value to set. 19695 * @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation. 19696 */ 19697 prop: function(name, value) { 19698 var self = this, undef, item; 19699 19700 if (value !== undef) { 19701 self.each(function(item) { 19702 if (item[name]) { 19703 item[name](value); 19704 } 19705 }); 19706 19707 return self; 19708 } 19709 19710 item = self[0]; 19711 19712 if (item && item[name]) { 19713 return item[name](); 19714 } 19715 }, 19716 19717 /** 19718 * Executes the specific function name with optional arguments an all items in collection if it exists. 19719 * 19720 * @example collection.exec("myMethod", arg1, arg2, arg3); 19721 * @method exec 19722 * @param {String} name Name of the function to execute. 19723 * @param {Object} ... Multiple arguments to pass to each function. 19724 * @return {tinymce.ui.Collection} Current collection. 19725 */ 19726 exec: function(name) { 19727 var self = this, args = Tools.toArray(arguments).slice(1); 19728 19729 self.each(function(item) { 19730 if (item[name]) { 19731 item[name].apply(item, args); 19732 } 19733 }); 19734 19735 return self; 19736 }, 19737 19738 /** 19739 * Remove all items from collection and DOM. 19740 * 19741 * @method remove 19742 * @return {tinymce.ui.Collection} Current collection. 19743 */ 19744 remove: function() { 19745 var i = this.length; 19746 19747 while (i--) { 19748 this[i].remove(); 19749 } 19750 19751 return this; 19752 } 19753 19754 /** 19755 * Fires the specified event by name and arguments on the control. This will execute all 19756 * bound event handlers. 19757 * 19758 * @method fire 19759 * @param {String} name Name of the event to fire. 19760 * @param {Object} args Optional arguments to pass to the event. 19761 * @return {tinymce.ui.Collection} Current collection instance. 19762 */ 19763 // fire: function(event, args) {}, -- Generated by code below 19764 19765 /** 19766 * Binds a callback to the specified event. This event can both be 19767 * native browser events like "click" or custom ones like PostRender. 19768 * 19769 * The callback function will have two parameters the first one being the control that received the event 19770 * the second one will be the event object either the browsers native event object or a custom JS object. 19771 * 19772 * @method on 19773 * @param {String} name Name of the event to bind. For example "click". 19774 * @param {String/function} callback Callback function to execute ones the event occurs. 19775 * @return {tinymce.ui.Collection} Current collection instance. 19776 */ 19777 // on: function(name, callback) {}, -- Generated by code below 19778 19779 /** 19780 * Unbinds the specified event and optionally a specific callback. If you omit the name 19781 * parameter all event handlers will be removed. If you omit the callback all event handles 19782 * by the specified name will be removed. 19783 * 19784 * @method off 19785 * @param {String} name Optional name for the event to unbind. 19786 * @param {function} callback Optional callback function to unbind. 19787 * @return {tinymce.ui.Collection} Current collection instance. 19788 */ 19789 // off: function(name, callback) {}, -- Generated by code below 19790 19791 /** 19792 * Shows the items in the current collection. 19793 * 19794 * @method show 19795 * @return {tinymce.ui.Collection} Current collection instance. 19796 */ 19797 // show: function() {}, -- Generated by code below 19798 19799 /** 19800 * Hides the items in the current collection. 19801 * 19802 * @method hide 19803 * @return {tinymce.ui.Collection} Current collection instance. 19804 */ 19805 // hide: function() {}, -- Generated by code below 19806 19807 /** 19808 * Sets/gets the text contents of the items in the current collection. 19809 * 19810 * @method text 19811 * @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation. 19812 */ 19813 // text: function(value) {}, -- Generated by code below 19814 19815 /** 19816 * Sets/gets the name contents of the items in the current collection. 19817 * 19818 * @method name 19819 * @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation. 19820 */ 19821 // name: function(value) {}, -- Generated by code below 19822 19823 /** 19824 * Sets/gets the disabled state on the items in the current collection. 19825 * 19826 * @method disabled 19827 * @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation. 19828 */ 19829 // disabled: function(state) {}, -- Generated by code below 19830 19831 /** 19832 * Sets/gets the active state on the items in the current collection. 19833 * 19834 * @method active 19835 * @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation. 19836 */ 19837 // active: function(state) {}, -- Generated by code below 19838 19839 /** 19840 * Sets/gets the selected state on the items in the current collection. 19841 * 19842 * @method selected 19843 * @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation. 19844 */ 19845 // selected: function(state) {}, -- Generated by code below 19846 19847 /** 19848 * Sets/gets the selected state on the items in the current collection. 19849 * 19850 * @method visible 19851 * @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation. 19852 */ 19853 // visible: function(state) {}, -- Generated by code below 19854 19855 /** 19856 * Adds a class to all items in the collection. 19857 * 19858 * @method addClass 19859 * @param {String} cls Class to add to each item. 19860 * @return {tinymce.ui.Collection} Current collection instance. 19861 */ 19862 // addClass: function(cls) {}, -- Generated by code below 19863 19864 /** 19865 * Removes the specified class from all items in collection. 19866 * 19867 * @method removeClass 19868 * @param {String} cls Class to remove from each item. 19869 * @return {tinymce.ui.Collection} Current collection instance. 19870 */ 19871 // removeClass: function(cls) {}, -- Generated by code below 19872 }; 19873 19874 // Extend tinymce.ui.Collection prototype with some generated control specific methods 19875 Tools.each('fire on off show hide addClass removeClass append prepend before after reflow'.split(' '), function(name) { 19876 proto[name] = function() { 19877 var args = Tools.toArray(arguments); 19878 19879 this.each(function(ctrl) { 19880 if (name in ctrl) { 19881 ctrl[name].apply(ctrl, args); 19882 } 19883 }); 19884 19885 return this; 19886 }; 19887 }); 19888 19889 // Extend tinymce.ui.Collection prototype with some property methods 19890 Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function(name) { 19891 proto[name] = function(value) { 19892 return this.prop(name, value); 19893 }; 19894 }); 19895 19896 // Create class based on the new prototype 19897 Collection = Class.extend(proto); 19898 19899 // Stick Collection into Selector to prevent circual references 19900 Selector.Collection = Collection; 19901 19902 return Collection; 19903 }); 19904 19905 // Included from: js/tinymce/classes/ui/DomUtils.js 19906 19907 /** 19908 * DOMUtils.js 19909 * 19910 * Copyright, Moxiecode Systems AB 19911 * Released under LGPL License. 19912 * 19913 * License: http://www.tinymce.com/license 19914 * Contributing: http://www.tinymce.com/contributing 19915 */ 19916 19917 define("tinymce/ui/DomUtils", [ 19918 "tinymce/util/Tools", 19919 "tinymce/dom/DOMUtils" 19920 ], function(Tools, DOMUtils) { 19921 "use strict"; 19922 19923 var count = 0; 19924 19925 return { 19926 id: function() { 19927 return 'mceu_' + (count++); 19928 }, 19929 19930 createFragment: function(html) { 19931 return DOMUtils.DOM.createFragment(html); 19932 }, 19933 19934 getWindowSize: function() { 19935 return DOMUtils.DOM.getViewPort(); 19936 }, 19937 19938 getSize: function(elm) { 19939 var width, height; 19940 19941 if (elm.getBoundingClientRect) { 19942 var rect = elm.getBoundingClientRect(); 19943 19944 width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth); 19945 height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight); 19946 } else { 19947 width = elm.offsetWidth; 19948 height = elm.offsetHeight; 19949 } 19950 19951 return {width: width, height: height}; 19952 }, 19953 19954 getPos: function(elm, root) { 19955 return DOMUtils.DOM.getPos(elm, root); 19956 }, 19957 19958 getViewPort: function(win) { 19959 return DOMUtils.DOM.getViewPort(win); 19960 }, 19961 19962 get: function(id) { 19963 return document.getElementById(id); 19964 }, 19965 19966 addClass : function(elm, cls) { 19967 return DOMUtils.DOM.addClass(elm, cls); 19968 }, 19969 19970 removeClass : function(elm, cls) { 19971 return DOMUtils.DOM.removeClass(elm, cls); 19972 }, 19973 19974 hasClass : function(elm, cls) { 19975 return DOMUtils.DOM.hasClass(elm, cls); 19976 }, 19977 19978 toggleClass: function(elm, cls, state) { 19979 return DOMUtils.DOM.toggleClass(elm, cls, state); 19980 }, 19981 19982 css: function(elm, name, value) { 19983 return DOMUtils.DOM.setStyle(elm, name, value); 19984 }, 19985 19986 getRuntimeStyle: function(elm, name) { 19987 return DOMUtils.DOM.getStyle(elm, name, true); 19988 }, 19989 19990 on: function(target, name, callback, scope) { 19991 return DOMUtils.DOM.bind(target, name, callback, scope); 19992 }, 19993 19994 off: function(target, name, callback) { 19995 return DOMUtils.DOM.unbind(target, name, callback); 19996 }, 19997 19998 fire: function(target, name, args) { 19999 return DOMUtils.DOM.fire(target, name, args); 20000 }, 20001 20002 innerHtml: function(elm, html) { 20003 // Workaround for <div> in <p> bug on IE 8 #6178 20004 DOMUtils.DOM.setHTML(elm, html); 20005 } 20006 }; 20007 }); 20008 20009 // Included from: js/tinymce/classes/ui/Control.js 20010 20011 /** 20012 * Control.js 20013 * 20014 * Copyright, Moxiecode Systems AB 20015 * Released under LGPL License. 20016 * 20017 * License: http://www.tinymce.com/license 20018 * Contributing: http://www.tinymce.com/contributing 20019 */ 20020 20021 /*eslint consistent-this:0 */ 20022 20023 /** 20024 * This is the base class for all controls and containers. All UI control instances inherit 20025 * from this one as it has the base logic needed by all of them. 20026 * 20027 * @class tinymce.ui.Control 20028 */ 20029 define("tinymce/ui/Control", [ 20030 "tinymce/util/Class", 20031 "tinymce/util/Tools", 20032 "tinymce/util/EventDispatcher", 20033 "tinymce/ui/Collection", 20034 "tinymce/ui/DomUtils" 20035 ], function(Class, Tools, EventDispatcher, Collection, DomUtils) { 20036 "use strict"; 20037 20038 var hasMouseWheelEventSupport = "onmousewheel" in document; 20039 var hasWheelEventSupport = false; 20040 var classPrefix = "mce-"; 20041 20042 function getEventDispatcher(obj) { 20043 if (!obj._eventDispatcher) { 20044 obj._eventDispatcher = new EventDispatcher({ 20045 scope: obj, 20046 toggleEvent: function(name, state) { 20047 if (state && EventDispatcher.isNative(name)) { 20048 if (!obj._nativeEvents) { 20049 obj._nativeEvents = {}; 20050 } 20051 20052 obj._nativeEvents[name] = true; 20053 20054 if (obj._rendered) { 20055 obj.bindPendingEvents(); 20056 } 20057 } 20058 } 20059 }); 20060 } 20061 20062 return obj._eventDispatcher; 20063 } 20064 20065 var Control = Class.extend({ 20066 Statics: { 20067 classPrefix: classPrefix 20068 }, 20069 20070 isRtl: function() { 20071 return Control.rtl; 20072 }, 20073 20074 /** 20075 * Class/id prefix to use for all controls. 20076 * 20077 * @final 20078 * @field {String} classPrefix 20079 */ 20080 classPrefix: classPrefix, 20081 20082 /** 20083 * Constructs a new control instance with the specified settings. 20084 * 20085 * @constructor 20086 * @param {Object} settings Name/value object with settings. 20087 * @setting {String} style Style CSS properties to add. 20088 * @setting {String} border Border box values example: 1 1 1 1 20089 * @setting {String} padding Padding box values example: 1 1 1 1 20090 * @setting {String} margin Margin box values example: 1 1 1 1 20091 * @setting {Number} minWidth Minimal width for the control. 20092 * @setting {Number} minHeight Minimal height for the control. 20093 * @setting {String} classes Space separated list of classes to add. 20094 * @setting {String} role WAI-ARIA role to use for control. 20095 * @setting {Boolean} hidden Is the control hidden by default. 20096 * @setting {Boolean} disabled Is the control disabled by default. 20097 * @setting {String} name Name of the control instance. 20098 */ 20099 init: function(settings) { 20100 var self = this, classes, i; 20101 20102 self.settings = settings = Tools.extend({}, self.Defaults, settings); 20103 20104 // Initial states 20105 self._id = settings.id || DomUtils.id(); 20106 self._text = self._name = ''; 20107 self._width = self._height = 0; 20108 self._aria = {role: settings.role}; 20109 this._elmCache = {}; 20110 20111 // Setup classes 20112 classes = settings.classes; 20113 if (classes) { 20114 classes = classes.split(' '); 20115 classes.map = {}; 20116 i = classes.length; 20117 while (i--) { 20118 classes.map[classes[i]] = true; 20119 } 20120 } 20121 20122 self._classes = classes || []; 20123 self.visible(true); 20124 20125 // Set some properties 20126 Tools.each('title text width height name classes visible disabled active value'.split(' '), function(name) { 20127 var value = settings[name], undef; 20128 20129 if (value !== undef) { 20130 self[name](value); 20131 } else if (self['_' + name] === undef) { 20132 self['_' + name] = false; 20133 } 20134 }); 20135 20136 self.on('click', function() { 20137 if (self.disabled()) { 20138 return false; 20139 } 20140 }); 20141 20142 // TODO: Is this needed duplicate code see above? 20143 if (settings.classes) { 20144 Tools.each(settings.classes.split(' '), function(cls) { 20145 self.addClass(cls); 20146 }); 20147 } 20148 20149 /** 20150 * Name/value object with settings for the current control. 20151 * 20152 * @field {Object} settings 20153 */ 20154 self.settings = settings; 20155 20156 self._borderBox = self.parseBox(settings.border); 20157 self._paddingBox = self.parseBox(settings.padding); 20158 self._marginBox = self.parseBox(settings.margin); 20159 20160 if (settings.hidden) { 20161 self.hide(); 20162 } 20163 }, 20164 20165 // Will generate getter/setter methods for these properties 20166 Properties: 'parent,title,text,width,height,disabled,active,name,value', 20167 20168 // Will generate empty dummy functions for these 20169 Methods: 'renderHtml', 20170 20171 /** 20172 * Returns the root element to render controls into. 20173 * 20174 * @method getContainerElm 20175 * @return {Element} HTML DOM element to render into. 20176 */ 20177 getContainerElm: function() { 20178 return document.body; 20179 }, 20180 20181 /** 20182 * Returns a control instance for the current DOM element. 20183 * 20184 * @method getParentCtrl 20185 * @param {Element} elm HTML dom element to get parent control from. 20186 * @return {tinymce.ui.Control} Control instance or undefined. 20187 */ 20188 getParentCtrl: function(elm) { 20189 var ctrl, lookup = this.getRoot().controlIdLookup; 20190 20191 while (elm && lookup) { 20192 ctrl = lookup[elm.id]; 20193 if (ctrl) { 20194 break; 20195 } 20196 20197 elm = elm.parentNode; 20198 } 20199 20200 return ctrl; 20201 }, 20202 20203 /** 20204 * Parses the specified box value. A box value contains 1-4 properties in clockwise order. 20205 * 20206 * @method parseBox 20207 * @param {String/Number} value Box value "0 1 2 3" or "0" etc. 20208 * @return {Object} Object with top/right/bottom/left properties. 20209 * @private 20210 */ 20211 parseBox: function(value) { 20212 var len, radix = 10; 20213 20214 if (!value) { 20215 return; 20216 } 20217 20218 if (typeof(value) === "number") { 20219 value = value || 0; 20220 20221 return { 20222 top: value, 20223 left: value, 20224 bottom: value, 20225 right: value 20226 }; 20227 } 20228 20229 value = value.split(' '); 20230 len = value.length; 20231 20232 if (len === 1) { 20233 value[1] = value[2] = value[3] = value[0]; 20234 } else if (len === 2) { 20235 value[2] = value[0]; 20236 value[3] = value[1]; 20237 } else if (len === 3) { 20238 value[3] = value[1]; 20239 } 20240 20241 return { 20242 top: parseInt(value[0], radix) || 0, 20243 right: parseInt(value[1], radix) || 0, 20244 bottom: parseInt(value[2], radix) || 0, 20245 left: parseInt(value[3], radix) || 0 20246 }; 20247 }, 20248 20249 borderBox: function() { 20250 return this._borderBox; 20251 }, 20252 20253 paddingBox: function() { 20254 return this._paddingBox; 20255 }, 20256 20257 marginBox: function() { 20258 return this._marginBox; 20259 }, 20260 20261 measureBox: function(elm, prefix) { 20262 function getStyle(name) { 20263 var defaultView = document.defaultView; 20264 20265 if (defaultView) { 20266 // Remove camelcase 20267 name = name.replace(/[A-Z]/g, function(a) { 20268 return '-' + a; 20269 }); 20270 20271 return defaultView.getComputedStyle(elm, null).getPropertyValue(name); 20272 } 20273 20274 return elm.currentStyle[name]; 20275 } 20276 20277 function getSide(name) { 20278 var val = parseFloat(getStyle(name), 10); 20279 20280 return isNaN(val) ? 0 : val; 20281 } 20282 20283 return { 20284 top: getSide(prefix + "TopWidth"), 20285 right: getSide(prefix + "RightWidth"), 20286 bottom: getSide(prefix + "BottomWidth"), 20287 left: getSide(prefix + "LeftWidth") 20288 }; 20289 }, 20290 20291 /** 20292 * Initializes the current controls layout rect. 20293 * This will be executed by the layout managers to determine the 20294 * default minWidth/minHeight etc. 20295 * 20296 * @method initLayoutRect 20297 * @return {Object} Layout rect instance. 20298 */ 20299 initLayoutRect: function() { 20300 var self = this, settings = self.settings, borderBox, layoutRect; 20301 var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; 20302 var startMinWidth, startMinHeight, initialSize; 20303 20304 // Measure the current element 20305 borderBox = self._borderBox = self._borderBox || self.measureBox(elm, 'border'); 20306 self._paddingBox = self._paddingBox || self.measureBox(elm, 'padding'); 20307 self._marginBox = self._marginBox || self.measureBox(elm, 'margin'); 20308 initialSize = DomUtils.getSize(elm); 20309 20310 // Setup minWidth/minHeight and width/height 20311 startMinWidth = settings.minWidth; 20312 startMinHeight = settings.minHeight; 20313 minWidth = startMinWidth || initialSize.width; 20314 minHeight = startMinHeight || initialSize.height; 20315 width = settings.width; 20316 height = settings.height; 20317 autoResize = settings.autoResize; 20318 autoResize = typeof(autoResize) != "undefined" ? autoResize : !width && !height; 20319 20320 width = width || minWidth; 20321 height = height || minHeight; 20322 20323 var deltaW = borderBox.left + borderBox.right; 20324 var deltaH = borderBox.top + borderBox.bottom; 20325 20326 var maxW = settings.maxWidth || 0xFFFF; 20327 var maxH = settings.maxHeight || 0xFFFF; 20328 20329 // Setup initial layout rect 20330 self._layoutRect = layoutRect = { 20331 x: settings.x || 0, 20332 y: settings.y || 0, 20333 w: width, 20334 h: height, 20335 deltaW: deltaW, 20336 deltaH: deltaH, 20337 contentW: width - deltaW, 20338 contentH: height - deltaH, 20339 innerW: width - deltaW, 20340 innerH: height - deltaH, 20341 startMinWidth: startMinWidth || 0, 20342 startMinHeight: startMinHeight || 0, 20343 minW: Math.min(minWidth, maxW), 20344 minH: Math.min(minHeight, maxH), 20345 maxW: maxW, 20346 maxH: maxH, 20347 autoResize: autoResize, 20348 scrollW: 0 20349 }; 20350 20351 self._lastLayoutRect = {}; 20352 20353 return layoutRect; 20354 }, 20355 20356 /** 20357 * Getter/setter for the current layout rect. 20358 * 20359 * @method layoutRect 20360 * @param {Object} [newRect] Optional new layout rect. 20361 * @return {tinymce.ui.Control/Object} Current control or rect object. 20362 */ 20363 layoutRect: function(newRect) { 20364 var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; 20365 20366 // Initialize default layout rect 20367 if (!curRect) { 20368 curRect = self.initLayoutRect(); 20369 } 20370 20371 // Set new rect values 20372 if (newRect) { 20373 // Calc deltas between inner and outer sizes 20374 deltaWidth = curRect.deltaW; 20375 deltaHeight = curRect.deltaH; 20376 20377 // Set x position 20378 if (newRect.x !== undef) { 20379 curRect.x = newRect.x; 20380 } 20381 20382 // Set y position 20383 if (newRect.y !== undef) { 20384 curRect.y = newRect.y; 20385 } 20386 20387 // Set minW 20388 if (newRect.minW !== undef) { 20389 curRect.minW = newRect.minW; 20390 } 20391 20392 // Set minH 20393 if (newRect.minH !== undef) { 20394 curRect.minH = newRect.minH; 20395 } 20396 20397 // Set new width and calculate inner width 20398 size = newRect.w; 20399 if (size !== undef) { 20400 size = size < curRect.minW ? curRect.minW : size; 20401 size = size > curRect.maxW ? curRect.maxW : size; 20402 curRect.w = size; 20403 curRect.innerW = size - deltaWidth; 20404 } 20405 20406 // Set new height and calculate inner height 20407 size = newRect.h; 20408 if (size !== undef) { 20409 size = size < curRect.minH ? curRect.minH : size; 20410 size = size > curRect.maxH ? curRect.maxH : size; 20411 curRect.h = size; 20412 curRect.innerH = size - deltaHeight; 20413 } 20414 20415 // Set new inner width and calculate width 20416 size = newRect.innerW; 20417 if (size !== undef) { 20418 size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; 20419 size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; 20420 curRect.innerW = size; 20421 curRect.w = size + deltaWidth; 20422 } 20423 20424 // Set new height and calculate inner height 20425 size = newRect.innerH; 20426 if (size !== undef) { 20427 size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; 20428 size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; 20429 curRect.innerH = size; 20430 curRect.h = size + deltaHeight; 20431 } 20432 20433 // Set new contentW 20434 if (newRect.contentW !== undef) { 20435 curRect.contentW = newRect.contentW; 20436 } 20437 20438 // Set new contentH 20439 if (newRect.contentH !== undef) { 20440 curRect.contentH = newRect.contentH; 20441 } 20442 20443 // Compare last layout rect with the current one to see if we need to repaint or not 20444 lastLayoutRect = self._lastLayoutRect; 20445 if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || 20446 lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { 20447 repaintControls = Control.repaintControls; 20448 20449 if (repaintControls) { 20450 if (repaintControls.map && !repaintControls.map[self._id]) { 20451 repaintControls.push(self); 20452 repaintControls.map[self._id] = true; 20453 } 20454 } 20455 20456 lastLayoutRect.x = curRect.x; 20457 lastLayoutRect.y = curRect.y; 20458 lastLayoutRect.w = curRect.w; 20459 lastLayoutRect.h = curRect.h; 20460 } 20461 20462 return self; 20463 } 20464 20465 return curRect; 20466 }, 20467 20468 /** 20469 * Repaints the control after a layout operation. 20470 * 20471 * @method repaint 20472 */ 20473 repaint: function() { 20474 var self = this, style, bodyStyle, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect, round; 20475 20476 // Use Math.round on all values on IE < 9 20477 round = !document.createRange ? Math.round : function(value) { 20478 return value; 20479 }; 20480 20481 style = self.getEl().style; 20482 rect = self._layoutRect; 20483 lastRepaintRect = self._lastRepaintRect || {}; 20484 20485 borderBox = self._borderBox; 20486 borderW = borderBox.left + borderBox.right; 20487 borderH = borderBox.top + borderBox.bottom; 20488 20489 if (rect.x !== lastRepaintRect.x) { 20490 style.left = round(rect.x) + 'px'; 20491 lastRepaintRect.x = rect.x; 20492 } 20493 20494 if (rect.y !== lastRepaintRect.y) { 20495 style.top = round(rect.y) + 'px'; 20496 lastRepaintRect.y = rect.y; 20497 } 20498 20499 if (rect.w !== lastRepaintRect.w) { 20500 style.width = round(rect.w - borderW) + 'px'; 20501 lastRepaintRect.w = rect.w; 20502 } 20503 20504 if (rect.h !== lastRepaintRect.h) { 20505 style.height = round(rect.h - borderH) + 'px'; 20506 lastRepaintRect.h = rect.h; 20507 } 20508 20509 // Update body if needed 20510 if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { 20511 bodyStyle = self.getEl('body').style; 20512 bodyStyle.width = round(rect.innerW) + 'px'; 20513 lastRepaintRect.innerW = rect.innerW; 20514 } 20515 20516 if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { 20517 bodyStyle = bodyStyle || self.getEl('body').style; 20518 bodyStyle.height = round(rect.innerH) + 'px'; 20519 lastRepaintRect.innerH = rect.innerH; 20520 } 20521 20522 self._lastRepaintRect = lastRepaintRect; 20523 self.fire('repaint', {}, false); 20524 }, 20525 20526 /** 20527 * Binds a callback to the specified event. This event can both be 20528 * native browser events like "click" or custom ones like PostRender. 20529 * 20530 * The callback function will be passed a DOM event like object that enables yout do stop propagation. 20531 * 20532 * @method on 20533 * @param {String} name Name of the event to bind. For example "click". 20534 * @param {String/function} callback Callback function to execute ones the event occurs. 20535 * @return {tinymce.ui.Control} Current control object. 20536 */ 20537 on: function(name, callback) { 20538 var self = this; 20539 20540 function resolveCallbackName(name) { 20541 var callback, scope; 20542 20543 if (typeof(name) != 'string') { 20544 return name; 20545 } 20546 20547 return function(e) { 20548 if (!callback) { 20549 self.parentsAndSelf().each(function(ctrl) { 20550 var callbacks = ctrl.settings.callbacks; 20551 20552 if (callbacks && (callback = callbacks[name])) { 20553 scope = ctrl; 20554 return false; 20555 } 20556 }); 20557 } 20558 20559 return callback.call(scope, e); 20560 }; 20561 } 20562 20563 getEventDispatcher(self).on(name, resolveCallbackName(callback)); 20564 20565 return self; 20566 }, 20567 20568 /** 20569 * Unbinds the specified event and optionally a specific callback. If you omit the name 20570 * parameter all event handlers will be removed. If you omit the callback all event handles 20571 * by the specified name will be removed. 20572 * 20573 * @method off 20574 * @param {String} [name] Name for the event to unbind. 20575 * @param {function} [callback] Callback function to unbind. 20576 * @return {mxex.ui.Control} Current control object. 20577 */ 20578 off: function(name, callback) { 20579 getEventDispatcher(this).off(name, callback); 20580 return this; 20581 }, 20582 20583 /** 20584 * Fires the specified event by name and arguments on the control. This will execute all 20585 * bound event handlers. 20586 * 20587 * @method fire 20588 * @param {String} name Name of the event to fire. 20589 * @param {Object} [args] Arguments to pass to the event. 20590 * @param {Boolean} [bubble] Value to control bubbeling. Defaults to true. 20591 * @return {Object} Current arguments object. 20592 */ 20593 fire: function(name, args, bubble) { 20594 var self = this; 20595 20596 args = args || {}; 20597 20598 if (!args.control) { 20599 args.control = self; 20600 } 20601 20602 args = getEventDispatcher(self).fire(name, args); 20603 20604 // Bubble event up to parents 20605 if (bubble !== false && self.parent) { 20606 var parent = self.parent(); 20607 while (parent && !args.isPropagationStopped()) { 20608 parent.fire(name, args, false); 20609 parent = parent.parent(); 20610 } 20611 } 20612 20613 return args; 20614 }, 20615 20616 /** 20617 * Returns true/false if the specified event has any listeners. 20618 * 20619 * @method hasEventListeners 20620 * @param {String} name Name of the event to check for. 20621 * @return {Boolean} True/false state if the event has listeners. 20622 */ 20623 hasEventListeners: function(name) { 20624 return getEventDispatcher(this).has(name); 20625 }, 20626 20627 /** 20628 * Returns a control collection with all parent controls. 20629 * 20630 * @method parents 20631 * @param {String} selector Optional selector expression to find parents. 20632 * @return {tinymce.ui.Collection} Collection with all parent controls. 20633 */ 20634 parents: function(selector) { 20635 var self = this, ctrl, parents = new Collection(); 20636 20637 // Add each parent to collection 20638 for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { 20639 parents.add(ctrl); 20640 } 20641 20642 // Filter away everything that doesn't match the selector 20643 if (selector) { 20644 parents = parents.filter(selector); 20645 } 20646 20647 return parents; 20648 }, 20649 20650 /** 20651 * Returns the current control and it's parents. 20652 * 20653 * @method parentsAndSelf 20654 * @param {String} selector Optional selector expression to find parents. 20655 * @return {tinymce.ui.Collection} Collection with all parent controls. 20656 */ 20657 parentsAndSelf: function(selector) { 20658 return new Collection(this).add(this.parents(selector)); 20659 }, 20660 20661 /** 20662 * Returns the control next to the current control. 20663 * 20664 * @method next 20665 * @return {tinymce.ui.Control} Next control instance. 20666 */ 20667 next: function() { 20668 var parentControls = this.parent().items(); 20669 20670 return parentControls[parentControls.indexOf(this) + 1]; 20671 }, 20672 20673 /** 20674 * Returns the control previous to the current control. 20675 * 20676 * @method prev 20677 * @return {tinymce.ui.Control} Previous control instance. 20678 */ 20679 prev: function() { 20680 var parentControls = this.parent().items(); 20681 20682 return parentControls[parentControls.indexOf(this) - 1]; 20683 }, 20684 20685 /** 20686 * Find the common ancestor for two control instances. 20687 * 20688 * @method findCommonAncestor 20689 * @param {tinymce.ui.Control} ctrl1 First control. 20690 * @param {tinymce.ui.Control} ctrl2 Second control. 20691 * @return {tinymce.ui.Control} Ancestor control instance. 20692 */ 20693 findCommonAncestor: function(ctrl1, ctrl2) { 20694 var parentCtrl; 20695 20696 while (ctrl1) { 20697 parentCtrl = ctrl2; 20698 20699 while (parentCtrl && ctrl1 != parentCtrl) { 20700 parentCtrl = parentCtrl.parent(); 20701 } 20702 20703 if (ctrl1 == parentCtrl) { 20704 break; 20705 } 20706 20707 ctrl1 = ctrl1.parent(); 20708 } 20709 20710 return ctrl1; 20711 }, 20712 20713 /** 20714 * Returns true/false if the specific control has the specific class. 20715 * 20716 * @method hasClass 20717 * @param {String} cls Class to check for. 20718 * @param {String} [group] Sub element group name. 20719 * @return {Boolean} True/false if the control has the specified class. 20720 */ 20721 hasClass: function(cls, group) { 20722 var classes = this._classes[group || 'control']; 20723 20724 cls = this.classPrefix + cls; 20725 20726 return classes && !!classes.map[cls]; 20727 }, 20728 20729 /** 20730 * Adds the specified class to the control 20731 * 20732 * @method addClass 20733 * @param {String} cls Class to check for. 20734 * @param {String} [group] Sub element group name. 20735 * @return {tinymce.ui.Control} Current control object. 20736 */ 20737 addClass: function(cls, group) { 20738 var self = this, classes, elm; 20739 20740 cls = this.classPrefix + cls; 20741 classes = self._classes[group || 'control']; 20742 20743 if (!classes) { 20744 classes = []; 20745 classes.map = {}; 20746 self._classes[group || 'control'] = classes; 20747 } 20748 20749 if (!classes.map[cls]) { 20750 classes.map[cls] = cls; 20751 classes.push(cls); 20752 20753 if (self._rendered) { 20754 elm = self.getEl(group); 20755 20756 if (elm) { 20757 elm.className = classes.join(' '); 20758 } 20759 } 20760 } 20761 20762 return self; 20763 }, 20764 20765 /** 20766 * Removes the specified class from the control. 20767 * 20768 * @method removeClass 20769 * @param {String} cls Class to remove. 20770 * @param {String} [group] Sub element group name. 20771 * @return {tinymce.ui.Control} Current control object. 20772 */ 20773 removeClass: function(cls, group) { 20774 var self = this, classes, i, elm; 20775 20776 cls = this.classPrefix + cls; 20777 classes = self._classes[group || 'control']; 20778 if (classes && classes.map[cls]) { 20779 delete classes.map[cls]; 20780 20781 i = classes.length; 20782 while (i--) { 20783 if (classes[i] === cls) { 20784 classes.splice(i, 1); 20785 } 20786 } 20787 } 20788 20789 if (self._rendered) { 20790 elm = self.getEl(group); 20791 20792 if (elm) { 20793 elm.className = classes.join(' '); 20794 } 20795 } 20796 20797 return self; 20798 }, 20799 20800 /** 20801 * Toggles the specified class on the control. 20802 * 20803 * @method toggleClass 20804 * @param {String} cls Class to remove. 20805 * @param {Boolean} state True/false state to add/remove class. 20806 * @param {String} [group] Sub element group name. 20807 * @return {tinymce.ui.Control} Current control object. 20808 */ 20809 toggleClass: function(cls, state, group) { 20810 var self = this; 20811 20812 if (state) { 20813 self.addClass(cls, group); 20814 } else { 20815 self.removeClass(cls, group); 20816 } 20817 20818 return self; 20819 }, 20820 20821 /** 20822 * Returns the class string for the specified group name. 20823 * 20824 * @method classes 20825 * @param {String} [group] Group to get clases by. 20826 * @return {String} Classes for the specified group. 20827 */ 20828 classes: function(group) { 20829 var classes = this._classes[group || 'control']; 20830 20831 return classes ? classes.join(' ') : ''; 20832 }, 20833 20834 /** 20835 * Sets the inner HTML of the control element. 20836 * 20837 * @method innerHtml 20838 * @param {String} html Html string to set as inner html. 20839 * @return {tinymce.ui.Control} Current control object. 20840 */ 20841 innerHtml: function(html) { 20842 DomUtils.innerHtml(this.getEl(), html); 20843 return this; 20844 }, 20845 20846 /** 20847 * Returns the control DOM element or sub element. 20848 * 20849 * @method getEl 20850 * @param {String} [suffix] Suffix to get element by. 20851 * @return {Element} HTML DOM element for the current control or it's children. 20852 */ 20853 getEl: function(suffix) { 20854 var id = suffix ? this._id + '-' + suffix : this._id; 20855 20856 if (!this._elmCache[id]) { 20857 this._elmCache[id] = DomUtils.get(id); 20858 } 20859 20860 return this._elmCache[id]; 20861 }, 20862 20863 /** 20864 * Sets/gets the visible for the control. 20865 * 20866 * @method visible 20867 * @param {Boolean} state Value to set to control. 20868 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 20869 */ 20870 visible: function(state) { 20871 var self = this, parentCtrl; 20872 20873 if (typeof(state) !== "undefined") { 20874 if (self._visible !== state) { 20875 if (self._rendered) { 20876 self.getEl().style.display = state ? '' : 'none'; 20877 } 20878 20879 self._visible = state; 20880 20881 // Parent container needs to reflow 20882 parentCtrl = self.parent(); 20883 if (parentCtrl) { 20884 parentCtrl._lastRect = null; 20885 } 20886 20887 self.fire(state ? 'show' : 'hide'); 20888 } 20889 20890 return self; 20891 } 20892 20893 return self._visible; 20894 }, 20895 20896 /** 20897 * Sets the visible state to true. 20898 * 20899 * @method show 20900 * @return {tinymce.ui.Control} Current control instance. 20901 */ 20902 show: function() { 20903 return this.visible(true); 20904 }, 20905 20906 /** 20907 * Sets the visible state to false. 20908 * 20909 * @method hide 20910 * @return {tinymce.ui.Control} Current control instance. 20911 */ 20912 hide: function() { 20913 return this.visible(false); 20914 }, 20915 20916 /** 20917 * Focuses the current control. 20918 * 20919 * @method focus 20920 * @return {tinymce.ui.Control} Current control instance. 20921 */ 20922 focus: function() { 20923 try { 20924 this.getEl().focus(); 20925 } catch (ex) { 20926 // Ignore IE error 20927 } 20928 20929 return this; 20930 }, 20931 20932 /** 20933 * Blurs the current control. 20934 * 20935 * @method blur 20936 * @return {tinymce.ui.Control} Current control instance. 20937 */ 20938 blur: function() { 20939 this.getEl().blur(); 20940 20941 return this; 20942 }, 20943 20944 /** 20945 * Sets the specified aria property. 20946 * 20947 * @method aria 20948 * @param {String} name Name of the aria property to set. 20949 * @param {String} value Value of the aria property. 20950 * @return {tinymce.ui.Control} Current control instance. 20951 */ 20952 aria: function(name, value) { 20953 var self = this, elm = self.getEl(self.ariaTarget); 20954 20955 if (typeof(value) === "undefined") { 20956 return self._aria[name]; 20957 } else { 20958 self._aria[name] = value; 20959 } 20960 20961 if (self._rendered) { 20962 elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); 20963 } 20964 20965 return self; 20966 }, 20967 20968 /** 20969 * Encodes the specified string with HTML entities. It will also 20970 * translate the string to different languages. 20971 * 20972 * @method encode 20973 * @param {String/Object/Array} text Text to entity encode. 20974 * @param {Boolean} [translate=true] False if the contents shouldn't be translated. 20975 * @return {String} Encoded and possible traslated string. 20976 */ 20977 encode: function(text, translate) { 20978 if (translate !== false) { 20979 text = this.translate(text); 20980 } 20981 20982 return (text || '').replace(/[&<>"]/g, function(match) { 20983 return '' + match.charCodeAt(0) + ';'; 20984 }); 20985 }, 20986 20987 /** 20988 * Returns the translated string. 20989 * 20990 * @method translate 20991 * @param {String} text Text to translate. 20992 * @return {String} Translated string or the same as the input. 20993 */ 20994 translate: function(text) { 20995 return Control.translate ? Control.translate(text) : text; 20996 }, 20997 20998 /** 20999 * Adds items before the current control. 21000 * 21001 * @method before 21002 * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. 21003 * @return {tinymce.ui.Control} Current control instance. 21004 */ 21005 before: function(items) { 21006 var self = this, parent = self.parent(); 21007 21008 if (parent) { 21009 parent.insert(items, parent.items().indexOf(self), true); 21010 } 21011 21012 return self; 21013 }, 21014 21015 /** 21016 * Adds items after the current control. 21017 * 21018 * @method after 21019 * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. 21020 * @return {tinymce.ui.Control} Current control instance. 21021 */ 21022 after: function(items) { 21023 var self = this, parent = self.parent(); 21024 21025 if (parent) { 21026 parent.insert(items, parent.items().indexOf(self)); 21027 } 21028 21029 return self; 21030 }, 21031 21032 /** 21033 * Removes the current control from DOM and from UI collections. 21034 * 21035 * @method remove 21036 * @return {tinymce.ui.Control} Current control instance. 21037 */ 21038 remove: function() { 21039 var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; 21040 21041 if (self.items) { 21042 var controls = self.items().toArray(); 21043 i = controls.length; 21044 while (i--) { 21045 controls[i].remove(); 21046 } 21047 } 21048 21049 if (parent && parent.items) { 21050 newItems = []; 21051 21052 parent.items().each(function(item) { 21053 if (item !== self) { 21054 newItems.push(item); 21055 } 21056 }); 21057 21058 parent.items().set(newItems); 21059 parent._lastRect = null; 21060 } 21061 21062 if (self._eventsRoot && self._eventsRoot == self) { 21063 DomUtils.off(elm); 21064 } 21065 21066 var lookup = self.getRoot().controlIdLookup; 21067 if (lookup) { 21068 delete lookup[self._id]; 21069 } 21070 21071 if (elm && elm.parentNode) { 21072 elm.parentNode.removeChild(elm); 21073 } 21074 21075 self._rendered = false; 21076 21077 return self; 21078 }, 21079 21080 /** 21081 * Renders the control before the specified element. 21082 * 21083 * @method renderBefore 21084 * @param {Element} elm Element to render before. 21085 * @return {tinymce.ui.Control} Current control instance. 21086 */ 21087 renderBefore: function(elm) { 21088 var self = this; 21089 21090 elm.parentNode.insertBefore(DomUtils.createFragment(self.renderHtml()), elm); 21091 self.postRender(); 21092 21093 return self; 21094 }, 21095 21096 /** 21097 * Renders the control to the specified element. 21098 * 21099 * @method renderBefore 21100 * @param {Element} elm Element to render to. 21101 * @return {tinymce.ui.Control} Current control instance. 21102 */ 21103 renderTo: function(elm) { 21104 var self = this; 21105 21106 elm = elm || self.getContainerElm(); 21107 elm.appendChild(DomUtils.createFragment(self.renderHtml())); 21108 self.postRender(); 21109 21110 return self; 21111 }, 21112 21113 /** 21114 * Post render method. Called after the control has been rendered to the target. 21115 * 21116 * @method postRender 21117 * @return {tinymce.ui.Control} Current control instance. 21118 */ 21119 postRender: function() { 21120 var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; 21121 21122 // Bind on<event> settings 21123 for (name in settings) { 21124 if (name.indexOf("on") === 0) { 21125 self.on(name.substr(2), settings[name]); 21126 } 21127 } 21128 21129 if (self._eventsRoot) { 21130 for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) { 21131 parentEventsRoot = parent._eventsRoot; 21132 } 21133 21134 if (parentEventsRoot) { 21135 for (name in parentEventsRoot._nativeEvents) { 21136 self._nativeEvents[name] = true; 21137 } 21138 } 21139 } 21140 21141 self.bindPendingEvents(); 21142 21143 if (settings.style) { 21144 elm = self.getEl(); 21145 if (elm) { 21146 elm.setAttribute('style', settings.style); 21147 elm.style.cssText = settings.style; 21148 } 21149 } 21150 21151 if (!self._visible) { 21152 DomUtils.css(self.getEl(), 'display', 'none'); 21153 } 21154 21155 if (self.settings.border) { 21156 box = self.borderBox(); 21157 DomUtils.css(self.getEl(), { 21158 'border-top-width': box.top, 21159 'border-right-width': box.right, 21160 'border-bottom-width': box.bottom, 21161 'border-left-width': box.left 21162 }); 21163 } 21164 21165 // Add instance to lookup 21166 var root = self.getRoot(); 21167 if (!root.controlIdLookup) { 21168 root.controlIdLookup = {}; 21169 } 21170 21171 root.controlIdLookup[self._id] = self; 21172 21173 for (var key in self._aria) { 21174 self.aria(key, self._aria[key]); 21175 } 21176 21177 self.fire('postrender', {}, false); 21178 }, 21179 21180 /** 21181 * Scrolls the current control into view. 21182 * 21183 * @method scrollIntoView 21184 * @param {String} align Alignment in view top|center|bottom. 21185 * @return {tinymce.ui.Control} Current control instance. 21186 */ 21187 scrollIntoView: function(align) { 21188 function getOffset(elm, rootElm) { 21189 var x, y, parent = elm; 21190 21191 x = y = 0; 21192 while (parent && parent != rootElm && parent.nodeType) { 21193 x += parent.offsetLeft || 0; 21194 y += parent.offsetTop || 0; 21195 parent = parent.offsetParent; 21196 } 21197 21198 return {x: x, y: y}; 21199 } 21200 21201 var elm = this.getEl(), parentElm = elm.parentNode; 21202 var x, y, width, height, parentWidth, parentHeight; 21203 var pos = getOffset(elm, parentElm); 21204 21205 x = pos.x; 21206 y = pos.y; 21207 width = elm.offsetWidth; 21208 height = elm.offsetHeight; 21209 parentWidth = parentElm.clientWidth; 21210 parentHeight = parentElm.clientHeight; 21211 21212 if (align == "end") { 21213 x -= parentWidth - width; 21214 y -= parentHeight - height; 21215 } else if (align == "center") { 21216 x -= (parentWidth / 2) - (width / 2); 21217 y -= (parentHeight / 2) - (height / 2); 21218 } 21219 21220 parentElm.scrollLeft = x; 21221 parentElm.scrollTop = y; 21222 21223 return this; 21224 }, 21225 21226 /** 21227 * Binds pending DOM events. 21228 * 21229 * @private 21230 */ 21231 bindPendingEvents: function() { 21232 var self = this, i, l, parents, eventRootCtrl, nativeEvents, name; 21233 21234 function delegate(e) { 21235 var control = self.getParentCtrl(e.target); 21236 21237 if (control) { 21238 control.fire(e.type, e); 21239 } 21240 } 21241 21242 function mouseLeaveHandler() { 21243 var ctrl = eventRootCtrl._lastHoverCtrl; 21244 21245 if (ctrl) { 21246 ctrl.fire("mouseleave", {target: ctrl.getEl()}); 21247 21248 ctrl.parents().each(function(ctrl) { 21249 ctrl.fire("mouseleave", {target: ctrl.getEl()}); 21250 }); 21251 21252 eventRootCtrl._lastHoverCtrl = null; 21253 } 21254 } 21255 21256 function mouseEnterHandler(e) { 21257 var ctrl = self.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents; 21258 21259 // Over on a new control 21260 if (ctrl !== lastCtrl) { 21261 eventRootCtrl._lastHoverCtrl = ctrl; 21262 21263 parents = ctrl.parents().toArray().reverse(); 21264 parents.push(ctrl); 21265 21266 if (lastCtrl) { 21267 lastParents = lastCtrl.parents().toArray().reverse(); 21268 lastParents.push(lastCtrl); 21269 21270 for (idx = 0; idx < lastParents.length; idx++) { 21271 if (parents[idx] !== lastParents[idx]) { 21272 break; 21273 } 21274 } 21275 21276 for (i = lastParents.length - 1; i >= idx; i--) { 21277 lastCtrl = lastParents[i]; 21278 lastCtrl.fire("mouseleave", { 21279 target : lastCtrl.getEl() 21280 }); 21281 } 21282 } 21283 21284 for (i = idx; i < parents.length; i++) { 21285 ctrl = parents[i]; 21286 ctrl.fire("mouseenter", { 21287 target : ctrl.getEl() 21288 }); 21289 } 21290 } 21291 } 21292 21293 function fixWheelEvent(e) { 21294 e.preventDefault(); 21295 21296 if (e.type == "mousewheel") { 21297 e.deltaY = -1 / 40 * e.wheelDelta; 21298 21299 if (e.wheelDeltaX) { 21300 e.deltaX = -1 / 40 * e.wheelDeltaX; 21301 } 21302 } else { 21303 e.deltaX = 0; 21304 e.deltaY = e.detail; 21305 } 21306 21307 e = self.fire("wheel", e); 21308 } 21309 21310 self._rendered = true; 21311 21312 nativeEvents = self._nativeEvents; 21313 if (nativeEvents) { 21314 // Find event root element if it exists 21315 parents = self.parents().toArray(); 21316 parents.unshift(self); 21317 for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) { 21318 eventRootCtrl = parents[i]._eventsRoot; 21319 } 21320 21321 // Event root wasn't found the use the root control 21322 if (!eventRootCtrl) { 21323 eventRootCtrl = parents[parents.length - 1] || self; 21324 } 21325 21326 // Set the eventsRoot property on children that didn't have it 21327 self._eventsRoot = eventRootCtrl; 21328 for (l = i, i = 0; i < l; i++) { 21329 parents[i]._eventsRoot = eventRootCtrl; 21330 } 21331 21332 var eventRootDelegates = eventRootCtrl._delegates; 21333 if (!eventRootDelegates) { 21334 eventRootDelegates = eventRootCtrl._delegates = {}; 21335 } 21336 21337 // Bind native event delegates 21338 for (name in nativeEvents) { 21339 if (!nativeEvents) { 21340 return false; 21341 } 21342 21343 if (name === "wheel" && !hasWheelEventSupport) { 21344 if (hasMouseWheelEventSupport) { 21345 DomUtils.on(self.getEl(), "mousewheel", fixWheelEvent); 21346 } else { 21347 DomUtils.on(self.getEl(), "DOMMouseScroll", fixWheelEvent); 21348 } 21349 21350 continue; 21351 } 21352 21353 // Special treatment for mousenter/mouseleave since these doesn't bubble 21354 if (name === "mouseenter" || name === "mouseleave") { 21355 // Fake mousenter/mouseleave 21356 if (!eventRootCtrl._hasMouseEnter) { 21357 DomUtils.on(eventRootCtrl.getEl(), "mouseleave", mouseLeaveHandler); 21358 DomUtils.on(eventRootCtrl.getEl(), "mouseover", mouseEnterHandler); 21359 eventRootCtrl._hasMouseEnter = 1; 21360 } 21361 } else if (!eventRootDelegates[name]) { 21362 DomUtils.on(eventRootCtrl.getEl(), name, delegate); 21363 eventRootDelegates[name] = true; 21364 } 21365 21366 // Remove the event once it's bound 21367 nativeEvents[name] = false; 21368 } 21369 } 21370 }, 21371 21372 getRoot: function() { 21373 var ctrl = this, rootControl, parents = []; 21374 21375 while (ctrl) { 21376 if (ctrl.rootControl) { 21377 rootControl = ctrl.rootControl; 21378 break; 21379 } 21380 21381 parents.push(ctrl); 21382 rootControl = ctrl; 21383 ctrl = ctrl.parent(); 21384 } 21385 21386 if (!rootControl) { 21387 rootControl = this; 21388 } 21389 21390 var i = parents.length; 21391 while (i--) { 21392 parents[i].rootControl = rootControl; 21393 } 21394 21395 return rootControl; 21396 }, 21397 21398 /** 21399 * Reflows the current control and it's parents. 21400 * This should be used after you for example append children to the current control so 21401 * that the layout managers know that they need to reposition everything. 21402 * 21403 * @example 21404 * container.append({type: 'button', text: 'My button'}).reflow(); 21405 * 21406 * @method reflow 21407 * @return {tinymce.ui.Control} Current control instance. 21408 */ 21409 reflow: function() { 21410 this.repaint(); 21411 21412 return this; 21413 } 21414 21415 /** 21416 * Sets/gets the parent container for the control. 21417 * 21418 * @method parent 21419 * @param {tinymce.ui.Container} parent Optional parent to set. 21420 * @return {tinymce.ui.Control} Parent control or the current control on a set action. 21421 */ 21422 // parent: function(parent) {} -- Generated 21423 21424 /** 21425 * Sets/gets the text for the control. 21426 * 21427 * @method text 21428 * @param {String} value Value to set to control. 21429 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 21430 */ 21431 // text: function(value) {} -- Generated 21432 21433 /** 21434 * Sets/gets the width for the control. 21435 * 21436 * @method width 21437 * @param {Number} value Value to set to control. 21438 * @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get. 21439 */ 21440 // width: function(value) {} -- Generated 21441 21442 /** 21443 * Sets/gets the height for the control. 21444 * 21445 * @method height 21446 * @param {Number} value Value to set to control. 21447 * @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get. 21448 */ 21449 // height: function(value) {} -- Generated 21450 21451 /** 21452 * Sets/gets the disabled state on the control. 21453 * 21454 * @method disabled 21455 * @param {Boolean} state Value to set to control. 21456 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 21457 */ 21458 // disabled: function(state) {} -- Generated 21459 21460 /** 21461 * Sets/gets the active for the control. 21462 * 21463 * @method active 21464 * @param {Boolean} state Value to set to control. 21465 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 21466 */ 21467 // active: function(state) {} -- Generated 21468 21469 /** 21470 * Sets/gets the name for the control. 21471 * 21472 * @method name 21473 * @param {String} value Value to set to control. 21474 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 21475 */ 21476 // name: function(value) {} -- Generated 21477 21478 /** 21479 * Sets/gets the title for the control. 21480 * 21481 * @method title 21482 * @param {String} value Value to set to control. 21483 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 21484 */ 21485 // title: function(value) {} -- Generated 21486 }); 21487 21488 return Control; 21489 }); 21490 21491 // Included from: js/tinymce/classes/ui/Factory.js 21492 21493 /** 21494 * Factory.js 21495 * 21496 * Copyright, Moxiecode Systems AB 21497 * Released under LGPL License. 21498 * 21499 * License: http://www.tinymce.com/license 21500 * Contributing: http://www.tinymce.com/contributing 21501 */ 21502 21503 /*global tinymce:true */ 21504 21505 /** 21506 * This class is a factory for control instances. This enables you 21507 * to create instances of controls without having to require the UI controls directly. 21508 * 21509 * It also allow you to override or add new control types. 21510 * 21511 * @class tinymce.ui.Factory 21512 */ 21513 define("tinymce/ui/Factory", [], function() { 21514 "use strict"; 21515 21516 var types = {}, namespaceInit; 21517 21518 return { 21519 /** 21520 * Adds a new control instance type to the factory. 21521 * 21522 * @method add 21523 * @param {String} type Type name for example "button". 21524 * @param {function} typeClass Class type function. 21525 */ 21526 add: function(type, typeClass) { 21527 types[type.toLowerCase()] = typeClass; 21528 }, 21529 21530 /** 21531 * Returns true/false if the specified type exists or not. 21532 * 21533 * @method has 21534 * @param {String} type Type to look for. 21535 * @return {Boolean} true/false if the control by name exists. 21536 */ 21537 has: function(type) { 21538 return !!types[type.toLowerCase()]; 21539 }, 21540 21541 /** 21542 * Creates a new control instance based on the settings provided. The instance created will be 21543 * based on the specified type property it can also create whole structures of components out of 21544 * the specified JSON object. 21545 * 21546 * @example 21547 * tinymce.ui.Factory.create({ 21548 * type: 'button', 21549 * text: 'Hello world!' 21550 * }); 21551 * 21552 * @method create 21553 * @param {Object/String} settings Name/Value object with items used to create the type. 21554 * @return {tinymce.ui.Control} Control instance based on the specified type. 21555 */ 21556 create: function(type, settings) { 21557 var ControlType, name, namespace; 21558 21559 // Build type lookup 21560 if (!namespaceInit) { 21561 namespace = tinymce.ui; 21562 21563 for (name in namespace) { 21564 types[name.toLowerCase()] = namespace[name]; 21565 } 21566 21567 namespaceInit = true; 21568 } 21569 21570 // If string is specified then use it as the type 21571 if (typeof(type) == 'string') { 21572 settings = settings || {}; 21573 settings.type = type; 21574 } else { 21575 settings = type; 21576 type = settings.type; 21577 } 21578 21579 // Find control type 21580 type = type.toLowerCase(); 21581 ControlType = types[type]; 21582 21583 // #if debug 21584 21585 if (!ControlType) { 21586 throw new Error("Could not find control by type: " + type); 21587 } 21588 21589 // #endif 21590 21591 ControlType = new ControlType(settings); 21592 ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine 21593 21594 return ControlType; 21595 } 21596 }; 21597 }); 21598 21599 // Included from: js/tinymce/classes/ui/KeyboardNavigation.js 21600 21601 /** 21602 * KeyboardNavigation.js 21603 * 21604 * Copyright, Moxiecode Systems AB 21605 * Released under LGPL License. 21606 * 21607 * License: http://www.tinymce.com/license 21608 * Contributing: http://www.tinymce.com/contributing 21609 */ 21610 21611 /** 21612 * This class handles keyboard navigation of controls and elements. 21613 * 21614 * @class tinymce.ui.KeyboardNavigation 21615 */ 21616 define("tinymce/ui/KeyboardNavigation", [ 21617 ], function() { 21618 "use strict"; 21619 21620 /** 21621 * This class handles all keyboard navigation for WAI-ARIA support. Each root container 21622 * gets an instance of this class. 21623 * 21624 * @constructor 21625 */ 21626 return function(settings) { 21627 var root = settings.root, focusedElement, focusedControl; 21628 21629 try { 21630 focusedElement = document.activeElement; 21631 } catch (ex) { 21632 // IE sometimes fails to return a proper element 21633 focusedElement = document.body; 21634 } 21635 21636 focusedControl = root.getParentCtrl(focusedElement); 21637 21638 /** 21639 * Returns the currently focused elements wai aria role of the currently 21640 * focused element or specified element. 21641 * 21642 * @private 21643 * @param {Element} elm Optional element to get role from. 21644 * @return {String} Role of specified element. 21645 */ 21646 function getRole(elm) { 21647 elm = elm || focusedElement; 21648 21649 return elm && elm.getAttribute('role'); 21650 } 21651 21652 /** 21653 * Returns the wai role of the parent element of the currently 21654 * focused element or specified element. 21655 * 21656 * @private 21657 * @param {Element} elm Optional element to get parent role from. 21658 * @return {String} Role of the first parent that has a role. 21659 */ 21660 function getParentRole(elm) { 21661 var role, parent = elm || focusedElement; 21662 21663 while ((parent = parent.parentNode)) { 21664 if ((role = getRole(parent))) { 21665 return role; 21666 } 21667 } 21668 } 21669 21670 /** 21671 * Returns a wai aria property by name for example aria-selected. 21672 * 21673 * @private 21674 * @param {String} name Name of the aria property to get for example "disabled". 21675 * @return {String} Aria property value. 21676 */ 21677 function getAriaProp(name) { 21678 var elm = focusedElement; 21679 21680 if (elm) { 21681 return elm.getAttribute('aria-' + name); 21682 } 21683 } 21684 21685 /** 21686 * Is the element a text input element or not. 21687 * 21688 * @private 21689 * @param {Element} elm Element to check if it's an text input element or not. 21690 * @return {Boolean} True/false if the element is a text element or not. 21691 */ 21692 function isTextInputElement(elm) { 21693 var tagName = elm.tagName.toUpperCase(); 21694 21695 // Notice: since type can be "email" etc we don't check the type 21696 // So all input elements gets treated as text input elements 21697 return tagName == "INPUT" || tagName == "TEXTAREA"; 21698 } 21699 21700 /** 21701 * Returns true/false if the specified element can be focused or not. 21702 * 21703 * @private 21704 * @param {Element} elm DOM element to check if it can be focused or not. 21705 * @return {Boolean} True/false if the element can have focus. 21706 */ 21707 function canFocus(elm) { 21708 if (isTextInputElement(elm) && !elm.hidden) { 21709 return true; 21710 } 21711 21712 if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) { 21713 return true; 21714 } 21715 21716 return false; 21717 } 21718 21719 /** 21720 * Returns an array of focusable visible elements within the specified container element. 21721 * 21722 * @private 21723 * @param {Element} elm DOM element to find focusable elements within. 21724 * @return {Array} Array of focusable elements. 21725 */ 21726 function getFocusElements(elm) { 21727 var elements = []; 21728 21729 function collect(elm) { 21730 if (elm.nodeType != 1 || elm.style.display == 'none') { 21731 return; 21732 } 21733 21734 if (canFocus(elm)) { 21735 elements.push(elm); 21736 } 21737 21738 for (var i = 0; i < elm.childNodes.length; i++) { 21739 collect(elm.childNodes[i]); 21740 } 21741 } 21742 21743 collect(elm || root.getEl()); 21744 21745 return elements; 21746 } 21747 21748 /** 21749 * Returns the navigation root control for the specified control. The navigation root 21750 * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group. 21751 * It will look for parents of the specified target control or the currently focused control if this option is omitted. 21752 * 21753 * @private 21754 * @param {tinymce.ui.Control} targetControl Optional target control to find root of. 21755 * @return {tinymce.ui.Control} Navigation root control. 21756 */ 21757 function getNavigationRoot(targetControl) { 21758 var navigationRoot, controls; 21759 21760 targetControl = targetControl || focusedControl; 21761 controls = targetControl.parents().toArray(); 21762 controls.unshift(targetControl); 21763 21764 for (var i = 0; i < controls.length; i++) { 21765 navigationRoot = controls[i]; 21766 21767 if (navigationRoot.settings.ariaRoot) { 21768 break; 21769 } 21770 } 21771 21772 return navigationRoot; 21773 } 21774 21775 /** 21776 * Focuses the first item in the specified targetControl element or the last aria index if the 21777 * navigation root has the ariaRemember option enabled. 21778 * 21779 * @private 21780 * @param {tinymce.ui.Control} targetControl Target control to focus the first item in. 21781 */ 21782 function focusFirst(targetControl) { 21783 var navigationRoot = getNavigationRoot(targetControl); 21784 var focusElements = getFocusElements(navigationRoot.getEl()); 21785 21786 if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) { 21787 moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements); 21788 } else { 21789 moveFocusToIndex(0, focusElements); 21790 } 21791 } 21792 21793 /** 21794 * Moves the focus to the specified index within the elements list. 21795 * This will scope the index to the size of the element list if it changed. 21796 * 21797 * @private 21798 * @param {Number} idx Specified index to move to. 21799 * @param {Array} elements Array with dom elements to move focus within. 21800 * @return {Number} Input index or a changed index if it was out of range. 21801 */ 21802 function moveFocusToIndex(idx, elements) { 21803 if (idx < 0) { 21804 idx = elements.length - 1; 21805 } else if (idx >= elements.length) { 21806 idx = 0; 21807 } 21808 21809 if (elements[idx]) { 21810 elements[idx].focus(); 21811 } 21812 21813 return idx; 21814 } 21815 21816 /** 21817 * Moves the focus forwards or backwards. 21818 * 21819 * @private 21820 * @param {Number} dir Direction to move in positive means forward, negative means backwards. 21821 * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements. 21822 * 21823 * @return {Boolean} Whether focus moved. 21824 */ 21825 function moveFocus(dir, elements) { 21826 var idx = -1, navigationRoot = getNavigationRoot(); 21827 21828 elements = elements || getFocusElements(navigationRoot.getEl()); 21829 21830 for (var i = 0; i < elements.length; i++) { 21831 if (elements[i] === focusedElement) { 21832 idx = i; 21833 } 21834 } 21835 21836 idx += dir; 21837 21838 if (!navigationRoot.settings.wrapFocus && (idx < 0 || idx >= elements.length)) { 21839 return false; 21840 } 21841 21842 navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements); 21843 21844 return true; 21845 } 21846 21847 /** 21848 * Moves the focus to the left this is called by the left key. 21849 * 21850 * @private 21851 */ 21852 function left() { 21853 var parentRole = getParentRole(); 21854 21855 if (parentRole == "tablist") { 21856 moveFocus(-1, getFocusElements(focusedElement.parentNode)); 21857 } else if (focusedControl.parent().submenu) { 21858 cancel(); 21859 } else { 21860 moveFocus(-1); 21861 } 21862 } 21863 21864 /** 21865 * Moves the focus to the right this is called by the right key. 21866 * 21867 * @private 21868 */ 21869 function right() { 21870 var role = getRole(), parentRole = getParentRole(); 21871 21872 if (parentRole == "tablist") { 21873 moveFocus(1, getFocusElements(focusedElement.parentNode)); 21874 } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) { 21875 enter(); 21876 } else { 21877 moveFocus(1); 21878 } 21879 } 21880 21881 /** 21882 * Moves the focus to the up this is called by the up key. 21883 * 21884 * @private 21885 */ 21886 function up() { 21887 moveFocus(-1); 21888 } 21889 21890 /** 21891 * Moves the focus to the up this is called by the down key. 21892 * 21893 * @private 21894 */ 21895 function down() { 21896 var role = getRole(), parentRole = getParentRole(); 21897 21898 if (role == "menuitem" && parentRole == "menubar") { 21899 enter(); 21900 } else if (role == "button" && getAriaProp('haspopup')) { 21901 enter({key: 'down'}); 21902 } else { 21903 moveFocus(1); 21904 } 21905 } 21906 21907 /** 21908 * Moves the focus to the next item or previous item depending on shift key. 21909 * 21910 * @private 21911 * @param {DOMEvent} e DOM event object. 21912 */ 21913 function tab(e) { 21914 var parentRole = getParentRole(); 21915 21916 if (parentRole == "tablist") { 21917 var elm = getFocusElements(focusedControl.getEl('body'))[0]; 21918 21919 if (elm) { 21920 elm.focus(); 21921 } 21922 21923 return true; 21924 } else { 21925 return moveFocus(e.shiftKey ? -1 : 1); 21926 } 21927 } 21928 21929 /** 21930 * Calls the cancel event on the currently focused control. This is normally done using the Esc key. 21931 * 21932 * @private 21933 */ 21934 function cancel() { 21935 return focusedControl.fire('cancel'); 21936 } 21937 21938 /** 21939 * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys. 21940 * 21941 * @private 21942 * @param {Object} aria Optional aria data to pass along with the enter event. 21943 */ 21944 function enter(aria) { 21945 aria = aria || {}; 21946 focusedControl.fire('click', {target: focusedElement, aria: aria}); 21947 } 21948 21949 root.on('keydown', function(e) { 21950 function handleNonTabOrEscEvent(e, handler) { 21951 // Ignore non tab keys for text elements 21952 if (isTextInputElement(focusedElement)) { 21953 return; 21954 } 21955 21956 if (handler(e) !== false) { 21957 e.preventDefault(); 21958 } 21959 } 21960 21961 if (e.isDefaultPrevented()) { 21962 return; 21963 } 21964 21965 switch (e.keyCode) { 21966 case 37: // DOM_VK_LEFT 21967 handleNonTabOrEscEvent(e, left); 21968 break; 21969 21970 case 39: // DOM_VK_RIGHT 21971 handleNonTabOrEscEvent(e, right); 21972 break; 21973 21974 case 38: // DOM_VK_UP 21975 handleNonTabOrEscEvent(e, up); 21976 break; 21977 21978 case 40: // DOM_VK_DOWN 21979 handleNonTabOrEscEvent(e, down); 21980 break; 21981 21982 case 27: // DOM_VK_ESCAPE 21983 var cancelEv = cancel(); 21984 if (cancelEv.isDefaultPrevented()) { 21985 e.preventDefault(); 21986 e.stopPropagation(); 21987 } 21988 if (cancelEv.isPropagationStopped()) { 21989 e.stopPropagation(); 21990 } 21991 break; 21992 21993 case 14: // DOM_VK_ENTER 21994 case 13: // DOM_VK_RETURN 21995 case 32: // DOM_VK_SPACE 21996 handleNonTabOrEscEvent(e, enter); 21997 break; 21998 21999 case 9: // DOM_VK_TAB 22000 if (tab(e) !== false) { 22001 e.preventDefault(); 22002 e.stopPropagation(); 22003 } 22004 break; 22005 } 22006 }); 22007 22008 root.on('focusin', function(e) { 22009 focusedElement = e.target; 22010 focusedControl = e.control; 22011 }); 22012 22013 return { 22014 focusFirst: focusFirst 22015 }; 22016 }; 22017 }); 22018 22019 // Included from: js/tinymce/classes/ui/Container.js 22020 22021 /** 22022 * Container.js 22023 * 22024 * Copyright, Moxiecode Systems AB 22025 * Released under LGPL License. 22026 * 22027 * License: http://www.tinymce.com/license 22028 * Contributing: http://www.tinymce.com/contributing 22029 */ 22030 22031 /** 22032 * Container control. This is extended by all controls that can have 22033 * children such as panels etc. You can also use this class directly as an 22034 * generic container instance. The container doesn't have any specific role or style. 22035 * 22036 * @-x-less Container.less 22037 * @class tinymce.ui.Container 22038 * @extends tinymce.ui.Control 22039 */ 22040 define("tinymce/ui/Container", [ 22041 "tinymce/ui/Control", 22042 "tinymce/ui/Collection", 22043 "tinymce/ui/Selector", 22044 "tinymce/ui/Factory", 22045 "tinymce/ui/KeyboardNavigation", 22046 "tinymce/util/Tools", 22047 "tinymce/ui/DomUtils" 22048 ], function(Control, Collection, Selector, Factory, KeyboardNavigation, Tools, DomUtils) { 22049 "use strict"; 22050 22051 var selectorCache = {}; 22052 22053 return Control.extend({ 22054 layout: '', 22055 innerClass: 'container-inner', 22056 22057 /** 22058 * Constructs a new control instance with the specified settings. 22059 * 22060 * @constructor 22061 * @param {Object} settings Name/value object with settings. 22062 * @setting {Array} items Items to add to container in JSON format or control instances. 22063 * @setting {String} layout Layout manager by name to use. 22064 * @setting {Object} defaults Default settings to apply to all items. 22065 */ 22066 init: function(settings) { 22067 var self = this; 22068 22069 self._super(settings); 22070 settings = self.settings; 22071 self._fixed = settings.fixed; 22072 self._items = new Collection(); 22073 22074 if (self.isRtl()) { 22075 self.addClass('rtl'); 22076 } 22077 22078 self.addClass('container'); 22079 self.addClass('container-body', 'body'); 22080 22081 if (settings.containerCls) { 22082 self.addClass(settings.containerCls); 22083 } 22084 22085 self._layout = Factory.create((settings.layout || self.layout) + 'layout'); 22086 22087 if (self.settings.items) { 22088 self.add(self.settings.items); 22089 } 22090 22091 // TODO: Fix this! 22092 self._hasBody = true; 22093 }, 22094 22095 /** 22096 * Returns a collection of child items that the container currently have. 22097 * 22098 * @method items 22099 * @return {tinymce.ui.Collection} Control collection direct child controls. 22100 */ 22101 items: function() { 22102 return this._items; 22103 }, 22104 22105 /** 22106 * Find child controls by selector. 22107 * 22108 * @method find 22109 * @param {String} selector Selector CSS pattern to find children by. 22110 * @return {tinymce.ui.Collection} Control collection with child controls. 22111 */ 22112 find: function(selector) { 22113 selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector); 22114 22115 return selector.find(this); 22116 }, 22117 22118 /** 22119 * Adds one or many items to the current container. This will create instances of 22120 * the object representations if needed. 22121 * 22122 * @method add 22123 * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container. 22124 * @return {tinymce.ui.Collection} Current collection control. 22125 */ 22126 add: function(items) { 22127 var self = this; 22128 22129 self.items().add(self.create(items)).parent(self); 22130 22131 return self; 22132 }, 22133 22134 /** 22135 * Focuses the current container instance. This will look 22136 * for the first control in the container and focus that. 22137 * 22138 * @method focus 22139 * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not. 22140 * @return {tinymce.ui.Collection} Current instance. 22141 */ 22142 focus: function(keyboard) { 22143 var self = this, focusCtrl, keyboardNav, items; 22144 22145 if (keyboard) { 22146 keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav; 22147 22148 if (keyboardNav) { 22149 keyboardNav.focusFirst(self); 22150 return; 22151 } 22152 } 22153 22154 items = self.find('*'); 22155 22156 // TODO: Figure out a better way to auto focus alert dialog buttons 22157 if (self.statusbar) { 22158 items.add(self.statusbar.items()); 22159 } 22160 22161 items.each(function(ctrl) { 22162 if (ctrl.settings.autofocus) { 22163 focusCtrl = null; 22164 return false; 22165 } 22166 22167 if (ctrl.canFocus) { 22168 focusCtrl = focusCtrl || ctrl; 22169 } 22170 }); 22171 22172 if (focusCtrl) { 22173 focusCtrl.focus(); 22174 } 22175 22176 return self; 22177 }, 22178 22179 /** 22180 * Replaces the specified child control with a new control. 22181 * 22182 * @method replace 22183 * @param {tinymce.ui.Control} oldItem Old item to be replaced. 22184 * @param {tinymce.ui.Control} newItem New item to be inserted. 22185 */ 22186 replace: function(oldItem, newItem) { 22187 var ctrlElm, items = this.items(), i = items.length; 22188 22189 // Replace the item in collection 22190 while (i--) { 22191 if (items[i] === oldItem) { 22192 items[i] = newItem; 22193 break; 22194 } 22195 } 22196 22197 if (i >= 0) { 22198 // Remove new item from DOM 22199 ctrlElm = newItem.getEl(); 22200 if (ctrlElm) { 22201 ctrlElm.parentNode.removeChild(ctrlElm); 22202 } 22203 22204 // Remove old item from DOM 22205 ctrlElm = oldItem.getEl(); 22206 if (ctrlElm) { 22207 ctrlElm.parentNode.removeChild(ctrlElm); 22208 } 22209 } 22210 22211 // Adopt the item 22212 newItem.parent(this); 22213 }, 22214 22215 /** 22216 * Creates the specified items. If any of the items is plain JSON style objects 22217 * it will convert these into real tinymce.ui.Control instances. 22218 * 22219 * @method create 22220 * @param {Array} items Array of items to convert into control instances. 22221 * @return {Array} Array with control instances. 22222 */ 22223 create: function(items) { 22224 var self = this, settings, ctrlItems = []; 22225 22226 // Non array structure, then force it into an array 22227 if (!Tools.isArray(items)) { 22228 items = [items]; 22229 } 22230 22231 // Add default type to each child control 22232 Tools.each(items, function(item) { 22233 if (item) { 22234 // Construct item if needed 22235 if (!(item instanceof Control)) { 22236 // Name only then convert it to an object 22237 if (typeof(item) == "string") { 22238 item = {type: item}; 22239 } 22240 22241 // Create control instance based on input settings and default settings 22242 settings = Tools.extend({}, self.settings.defaults, item); 22243 item.type = settings.type = settings.type || item.type || self.settings.defaultType || 22244 (settings.defaults ? settings.defaults.type : null); 22245 item = Factory.create(settings); 22246 } 22247 22248 ctrlItems.push(item); 22249 } 22250 }); 22251 22252 return ctrlItems; 22253 }, 22254 22255 /** 22256 * Renders new control instances. 22257 * 22258 * @private 22259 */ 22260 renderNew: function() { 22261 var self = this; 22262 22263 // Render any new items 22264 self.items().each(function(ctrl, index) { 22265 var containerElm, fragment; 22266 22267 ctrl.parent(self); 22268 22269 if (!ctrl._rendered) { 22270 containerElm = self.getEl('body'); 22271 fragment = DomUtils.createFragment(ctrl.renderHtml()); 22272 22273 // Insert or append the item 22274 if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) { 22275 containerElm.insertBefore(fragment, containerElm.childNodes[index]); 22276 } else { 22277 containerElm.appendChild(fragment); 22278 } 22279 22280 ctrl.postRender(); 22281 } 22282 }); 22283 22284 self._layout.applyClasses(self); 22285 self._lastRect = null; 22286 22287 return self; 22288 }, 22289 22290 /** 22291 * Appends new instances to the current container. 22292 * 22293 * @method append 22294 * @param {Array/tinymce.ui.Collection} items Array if controls to append. 22295 * @return {tinymce.ui.Container} Current container instance. 22296 */ 22297 append: function(items) { 22298 return this.add(items).renderNew(); 22299 }, 22300 22301 /** 22302 * Prepends new instances to the current container. 22303 * 22304 * @method prepend 22305 * @param {Array/tinymce.ui.Collection} items Array if controls to prepend. 22306 * @return {tinymce.ui.Container} Current container instance. 22307 */ 22308 prepend: function(items) { 22309 var self = this; 22310 22311 self.items().set(self.create(items).concat(self.items().toArray())); 22312 22313 return self.renderNew(); 22314 }, 22315 22316 /** 22317 * Inserts an control at a specific index. 22318 * 22319 * @method insert 22320 * @param {Array/tinymce.ui.Collection} items Array if controls to insert. 22321 * @param {Number} index Index to insert controls at. 22322 * @param {Boolean} [before=false] Inserts controls before the index. 22323 */ 22324 insert: function(items, index, before) { 22325 var self = this, curItems, beforeItems, afterItems; 22326 22327 items = self.create(items); 22328 curItems = self.items(); 22329 22330 if (!before && index < curItems.length - 1) { 22331 index += 1; 22332 } 22333 22334 if (index >= 0 && index < curItems.length) { 22335 beforeItems = curItems.slice(0, index).toArray(); 22336 afterItems = curItems.slice(index).toArray(); 22337 curItems.set(beforeItems.concat(items, afterItems)); 22338 } 22339 22340 return self.renderNew(); 22341 }, 22342 22343 /** 22344 * Populates the form fields from the specified JSON data object. 22345 * 22346 * Control items in the form that matches the data will have it's value set. 22347 * 22348 * @method fromJSON 22349 * @param {Object} data JSON data object to set control values by. 22350 * @return {tinymce.ui.Container} Current form instance. 22351 */ 22352 fromJSON: function(data) { 22353 var self = this; 22354 22355 for (var name in data) { 22356 self.find('#' + name).value(data[name]); 22357 } 22358 22359 return self; 22360 }, 22361 22362 /** 22363 * Serializes the form into a JSON object by getting all items 22364 * that has a name and a value. 22365 * 22366 * @method toJSON 22367 * @return {Object} JSON object with form data. 22368 */ 22369 toJSON: function() { 22370 var self = this, data = {}; 22371 22372 self.find('*').each(function(ctrl) { 22373 var name = ctrl.name(), value = ctrl.value(); 22374 22375 if (name && typeof(value) != "undefined") { 22376 data[name] = value; 22377 } 22378 }); 22379 22380 return data; 22381 }, 22382 22383 preRender: function() { 22384 }, 22385 22386 /** 22387 * Renders the control as a HTML string. 22388 * 22389 * @method renderHtml 22390 * @return {String} HTML representing the control. 22391 */ 22392 renderHtml: function() { 22393 var self = this, layout = self._layout, role = this.settings.role; 22394 22395 self.preRender(); 22396 layout.preRender(self); 22397 22398 return ( 22399 '<div id="' + self._id + '" class="' + self.classes() + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' + 22400 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 22401 (self.settings.html || '') + layout.renderHtml(self) + 22402 '</div>' + 22403 '</div>' 22404 ); 22405 }, 22406 22407 /** 22408 * Post render method. Called after the control has been rendered to the target. 22409 * 22410 * @method postRender 22411 * @return {tinymce.ui.Container} Current combobox instance. 22412 */ 22413 postRender: function() { 22414 var self = this, box; 22415 22416 self.items().exec('postRender'); 22417 self._super(); 22418 22419 self._layout.postRender(self); 22420 self._rendered = true; 22421 22422 if (self.settings.style) { 22423 DomUtils.css(self.getEl(), self.settings.style); 22424 } 22425 22426 if (self.settings.border) { 22427 box = self.borderBox(); 22428 DomUtils.css(self.getEl(), { 22429 'border-top-width': box.top, 22430 'border-right-width': box.right, 22431 'border-bottom-width': box.bottom, 22432 'border-left-width': box.left 22433 }); 22434 } 22435 22436 if (!self.parent()) { 22437 self.keyboardNav = new KeyboardNavigation({ 22438 root: self 22439 }); 22440 } 22441 22442 return self; 22443 }, 22444 22445 /** 22446 * Initializes the current controls layout rect. 22447 * This will be executed by the layout managers to determine the 22448 * default minWidth/minHeight etc. 22449 * 22450 * @method initLayoutRect 22451 * @return {Object} Layout rect instance. 22452 */ 22453 initLayoutRect: function() { 22454 var self = this, layoutRect = self._super(); 22455 22456 // Recalc container size by asking layout manager 22457 self._layout.recalc(self); 22458 22459 return layoutRect; 22460 }, 22461 22462 /** 22463 * Recalculates the positions of the controls in the current container. 22464 * This is invoked by the reflow method and shouldn't be called directly. 22465 * 22466 * @method recalc 22467 */ 22468 recalc: function() { 22469 var self = this, rect = self._layoutRect, lastRect = self._lastRect; 22470 22471 if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) { 22472 self._layout.recalc(self); 22473 rect = self.layoutRect(); 22474 self._lastRect = {x: rect.x, y: rect.y, w: rect.w, h: rect.h}; 22475 return true; 22476 } 22477 }, 22478 22479 /** 22480 * Reflows the current container and it's children and possible parents. 22481 * This should be used after you for example append children to the current control so 22482 * that the layout managers know that they need to reposition everything. 22483 * 22484 * @example 22485 * container.append({type: 'button', text: 'My button'}).reflow(); 22486 * 22487 * @method reflow 22488 * @return {tinymce.ui.Container} Current container instance. 22489 */ 22490 reflow: function() { 22491 var i; 22492 22493 if (this.visible()) { 22494 Control.repaintControls = []; 22495 Control.repaintControls.map = {}; 22496 22497 this.recalc(); 22498 i = Control.repaintControls.length; 22499 22500 while (i--) { 22501 Control.repaintControls[i].repaint(); 22502 } 22503 22504 // TODO: Fix me! 22505 if (this.settings.layout !== "flow" && this.settings.layout !== "stack") { 22506 this.repaint(); 22507 } 22508 22509 Control.repaintControls = []; 22510 } 22511 22512 return this; 22513 } 22514 }); 22515 }); 22516 22517 // Included from: js/tinymce/classes/ui/DragHelper.js 22518 22519 /** 22520 * DragHelper.js 22521 * 22522 * Copyright, Moxiecode Systems AB 22523 * Released under LGPL License. 22524 * 22525 * License: http://www.tinymce.com/license 22526 * Contributing: http://www.tinymce.com/contributing 22527 */ 22528 22529 /** 22530 * Drag/drop helper class. 22531 * 22532 * @example 22533 * var dragHelper = new tinymce.ui.DragHelper('mydiv', { 22534 * start: function(evt) { 22535 * }, 22536 * 22537 * drag: function(evt) { 22538 * }, 22539 * 22540 * end: function(evt) { 22541 * } 22542 * }); 22543 * 22544 * @class tinymce.ui.DragHelper 22545 */ 22546 define("tinymce/ui/DragHelper", [ 22547 "tinymce/ui/DomUtils" 22548 ], function(DomUtils) { 22549 "use strict"; 22550 22551 function getDocumentSize() { 22552 var doc = document, documentElement, body, scrollWidth, clientWidth; 22553 var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max; 22554 22555 documentElement = doc.documentElement; 22556 body = doc.body; 22557 22558 scrollWidth = max(documentElement.scrollWidth, body.scrollWidth); 22559 clientWidth = max(documentElement.clientWidth, body.clientWidth); 22560 offsetWidth = max(documentElement.offsetWidth, body.offsetWidth); 22561 22562 scrollHeight = max(documentElement.scrollHeight, body.scrollHeight); 22563 clientHeight = max(documentElement.clientHeight, body.clientHeight); 22564 offsetHeight = max(documentElement.offsetHeight, body.offsetHeight); 22565 22566 return { 22567 width: scrollWidth < offsetWidth ? clientWidth : scrollWidth, 22568 height: scrollHeight < offsetHeight ? clientHeight : scrollHeight 22569 }; 22570 } 22571 22572 return function(id, settings) { 22573 var eventOverlayElm, doc = document, downButton, start, stop, drag, startX, startY; 22574 22575 settings = settings || {}; 22576 22577 function getHandleElm() { 22578 return doc.getElementById(settings.handle || id); 22579 } 22580 22581 start = function(e) { 22582 var docSize = getDocumentSize(), handleElm, cursor; 22583 22584 e.preventDefault(); 22585 downButton = e.button; 22586 handleElm = getHandleElm(); 22587 startX = e.screenX; 22588 startY = e.screenY; 22589 22590 // Grab cursor from handle 22591 if (window.getComputedStyle) { 22592 cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor"); 22593 } else { 22594 cursor = handleElm.runtimeStyle.cursor; 22595 } 22596 22597 // Create event overlay and add it to document 22598 eventOverlayElm = doc.createElement('div'); 22599 DomUtils.css(eventOverlayElm, { 22600 position: "absolute", 22601 top: 0, left: 0, 22602 width: docSize.width, 22603 height: docSize.height, 22604 zIndex: 0x7FFFFFFF, 22605 opacity: 0.0001, 22606 cursor: cursor 22607 }); 22608 22609 doc.body.appendChild(eventOverlayElm); 22610 22611 // Bind mouse events 22612 DomUtils.on(doc, 'mousemove', drag); 22613 DomUtils.on(doc, 'mouseup', stop); 22614 22615 // Begin drag 22616 settings.start(e); 22617 }; 22618 22619 drag = function(e) { 22620 if (e.button !== downButton) { 22621 return stop(e); 22622 } 22623 22624 e.deltaX = e.screenX - startX; 22625 e.deltaY = e.screenY - startY; 22626 22627 e.preventDefault(); 22628 settings.drag(e); 22629 }; 22630 22631 stop = function(e) { 22632 DomUtils.off(doc, 'mousemove', drag); 22633 DomUtils.off(doc, 'mouseup', stop); 22634 22635 eventOverlayElm.parentNode.removeChild(eventOverlayElm); 22636 22637 if (settings.stop) { 22638 settings.stop(e); 22639 } 22640 }; 22641 22642 /** 22643 * Destroys the drag/drop helper instance. 22644 * 22645 * @method destroy 22646 */ 22647 this.destroy = function() { 22648 DomUtils.off(getHandleElm()); 22649 }; 22650 22651 DomUtils.on(getHandleElm(), 'mousedown', start); 22652 }; 22653 }); 22654 22655 // Included from: js/tinymce/classes/ui/Scrollable.js 22656 22657 /** 22658 * Scrollable.js 22659 * 22660 * Copyright, Moxiecode Systems AB 22661 * Released under LGPL License. 22662 * 22663 * License: http://www.tinymce.com/license 22664 * Contributing: http://www.tinymce.com/contributing 22665 */ 22666 22667 /** 22668 * This mixin makes controls scrollable using custom scrollbars. 22669 * 22670 * @-x-less Scrollable.less 22671 * @mixin tinymce.ui.Scrollable 22672 */ 22673 define("tinymce/ui/Scrollable", [ 22674 "tinymce/ui/DomUtils", 22675 "tinymce/ui/DragHelper" 22676 ], function(DomUtils, DragHelper) { 22677 "use strict"; 22678 22679 return { 22680 init: function() { 22681 var self = this; 22682 self.on('repaint', self.renderScroll); 22683 }, 22684 22685 renderScroll: function() { 22686 var self = this, margin = 2; 22687 22688 function repaintScroll() { 22689 var hasScrollH, hasScrollV, bodyElm; 22690 22691 function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) { 22692 var containerElm, scrollBarElm, scrollThumbElm; 22693 var containerSize, scrollSize, ratio, rect; 22694 var posNameLower, sizeNameLower; 22695 22696 scrollBarElm = self.getEl('scroll' + axisName); 22697 if (scrollBarElm) { 22698 posNameLower = posName.toLowerCase(); 22699 sizeNameLower = sizeName.toLowerCase(); 22700 22701 if (self.getEl('absend')) { 22702 DomUtils.css(self.getEl('absend'), posNameLower, self.layoutRect()[contentSizeName] - 1); 22703 } 22704 22705 if (!hasScroll) { 22706 DomUtils.css(scrollBarElm, 'display', 'none'); 22707 return; 22708 } 22709 22710 DomUtils.css(scrollBarElm, 'display', 'block'); 22711 containerElm = self.getEl('body'); 22712 scrollThumbElm = self.getEl('scroll' + axisName + "t"); 22713 containerSize = containerElm["client" + sizeName] - (margin * 2); 22714 containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0; 22715 scrollSize = containerElm["scroll" + sizeName]; 22716 ratio = containerSize / scrollSize; 22717 22718 rect = {}; 22719 rect[posNameLower] = containerElm["offset" + posName] + margin; 22720 rect[sizeNameLower] = containerSize; 22721 DomUtils.css(scrollBarElm, rect); 22722 22723 rect = {}; 22724 rect[posNameLower] = containerElm["scroll" + posName] * ratio; 22725 rect[sizeNameLower] = containerSize * ratio; 22726 DomUtils.css(scrollThumbElm, rect); 22727 } 22728 } 22729 22730 bodyElm = self.getEl('body'); 22731 hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth; 22732 hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight; 22733 22734 repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height"); 22735 repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width"); 22736 } 22737 22738 function addScroll() { 22739 function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) { 22740 var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix; 22741 22742 self.getEl().appendChild(DomUtils.createFragment( 22743 '<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' + 22744 '<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' + 22745 '</div>' 22746 )); 22747 22748 self.draghelper = new DragHelper(axisId + 't', { 22749 start: function() { 22750 scrollStart = self.getEl('body')["scroll" + posName]; 22751 DomUtils.addClass(DomUtils.get(axisId), prefix + 'active'); 22752 }, 22753 22754 drag: function(e) { 22755 var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect(); 22756 22757 hasScrollH = layoutRect.contentW > layoutRect.innerW; 22758 hasScrollV = layoutRect.contentH > layoutRect.innerH; 22759 containerSize = self.getEl('body')["client" + sizeName] - (margin * 2); 22760 containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0; 22761 22762 ratio = containerSize / self.getEl('body')["scroll" + sizeName]; 22763 self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio); 22764 }, 22765 22766 stop: function() { 22767 DomUtils.removeClass(DomUtils.get(axisId), prefix + 'active'); 22768 } 22769 }); 22770 /* 22771 self.on('click', function(e) { 22772 if (e.target.id == self._id + '-scrollv') { 22773 22774 } 22775 });*/ 22776 } 22777 22778 self.addClass('scroll'); 22779 22780 addScrollAxis("v", "Top", "Height", "Y", "Width"); 22781 addScrollAxis("h", "Left", "Width", "X", "Height"); 22782 } 22783 22784 if (self.settings.autoScroll) { 22785 if (!self._hasScroll) { 22786 self._hasScroll = true; 22787 addScroll(); 22788 22789 self.on('wheel', function(e) { 22790 var bodyEl = self.getEl('body'); 22791 22792 bodyEl.scrollLeft += (e.deltaX || 0) * 10; 22793 bodyEl.scrollTop += e.deltaY * 10; 22794 22795 repaintScroll(); 22796 }); 22797 22798 DomUtils.on(self.getEl('body'), "scroll", repaintScroll); 22799 } 22800 22801 repaintScroll(); 22802 } 22803 } 22804 }; 22805 }); 22806 22807 // Included from: js/tinymce/classes/ui/Panel.js 22808 22809 /** 22810 * Panel.js 22811 * 22812 * Copyright, Moxiecode Systems AB 22813 * Released under LGPL License. 22814 * 22815 * License: http://www.tinymce.com/license 22816 * Contributing: http://www.tinymce.com/contributing 22817 */ 22818 22819 /** 22820 * Creates a new panel. 22821 * 22822 * @-x-less Panel.less 22823 * @class tinymce.ui.Panel 22824 * @extends tinymce.ui.Container 22825 * @mixes tinymce.ui.Scrollable 22826 */ 22827 define("tinymce/ui/Panel", [ 22828 "tinymce/ui/Container", 22829 "tinymce/ui/Scrollable" 22830 ], function(Container, Scrollable) { 22831 "use strict"; 22832 22833 return Container.extend({ 22834 Defaults: { 22835 layout: 'fit', 22836 containerCls: 'panel' 22837 }, 22838 22839 Mixins: [Scrollable], 22840 22841 /** 22842 * Renders the control as a HTML string. 22843 * 22844 * @method renderHtml 22845 * @return {String} HTML representing the control. 22846 */ 22847 renderHtml: function() { 22848 var self = this, layout = self._layout, innerHtml = self.settings.html; 22849 22850 self.preRender(); 22851 layout.preRender(self); 22852 22853 if (typeof(innerHtml) == "undefined") { 22854 innerHtml = ( 22855 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 22856 layout.renderHtml(self) + 22857 '</div>' 22858 ); 22859 } else { 22860 if (typeof(innerHtml) == 'function') { 22861 innerHtml = innerHtml.call(self); 22862 } 22863 22864 self._hasBody = false; 22865 } 22866 22867 return ( 22868 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1" role="group">' + 22869 (self._preBodyHtml || '') + 22870 innerHtml + 22871 '</div>' 22872 ); 22873 } 22874 }); 22875 }); 22876 22877 // Included from: js/tinymce/classes/ui/Movable.js 22878 22879 /** 22880 * Movable.js 22881 * 22882 * Copyright, Moxiecode Systems AB 22883 * Released under LGPL License. 22884 * 22885 * License: http://www.tinymce.com/license 22886 * Contributing: http://www.tinymce.com/contributing 22887 */ 22888 22889 /** 22890 * Movable mixin. Makes controls movable absolute and relative to other elements. 22891 * 22892 * @mixin tinymce.ui.Movable 22893 */ 22894 define("tinymce/ui/Movable", [ 22895 "tinymce/ui/DomUtils" 22896 ], function(DomUtils) { 22897 "use strict"; 22898 22899 function calculateRelativePosition(ctrl, targetElm, rel) { 22900 var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size; 22901 22902 viewport = DomUtils.getViewPort(); 22903 22904 // Get pos of target 22905 pos = DomUtils.getPos(targetElm); 22906 x = pos.x; 22907 y = pos.y; 22908 22909 if (ctrl._fixed && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') { 22910 x -= viewport.x; 22911 y -= viewport.y; 22912 } 22913 22914 // Get size of self 22915 ctrlElm = ctrl.getEl(); 22916 size = DomUtils.getSize(ctrlElm); 22917 selfW = size.width; 22918 selfH = size.height; 22919 22920 // Get size of target 22921 size = DomUtils.getSize(targetElm); 22922 targetW = size.width; 22923 targetH = size.height; 22924 22925 // Parse align string 22926 rel = (rel || '').split(''); 22927 22928 // Target corners 22929 if (rel[0] === 'b') { 22930 y += targetH; 22931 } 22932 22933 if (rel[1] === 'r') { 22934 x += targetW; 22935 } 22936 22937 if (rel[0] === 'c') { 22938 y += Math.round(targetH / 2); 22939 } 22940 22941 if (rel[1] === 'c') { 22942 x += Math.round(targetW / 2); 22943 } 22944 22945 // Self corners 22946 if (rel[3] === 'b') { 22947 y -= selfH; 22948 } 22949 22950 if (rel[4] === 'r') { 22951 x -= selfW; 22952 } 22953 22954 if (rel[3] === 'c') { 22955 y -= Math.round(selfH / 2); 22956 } 22957 22958 if (rel[4] === 'c') { 22959 x -= Math.round(selfW / 2); 22960 } 22961 22962 return { 22963 x: x, 22964 y: y, 22965 w: selfW, 22966 h: selfH 22967 }; 22968 } 22969 22970 return { 22971 /** 22972 * Tests various positions to get the most suitable one. 22973 * 22974 * @method testMoveRel 22975 * @param {DOMElement} elm Element to position against. 22976 * @param {Array} rels Array with relative positions. 22977 * @return {String} Best suitable relative position. 22978 */ 22979 testMoveRel: function(elm, rels) { 22980 var viewPortRect = DomUtils.getViewPort(); 22981 22982 for (var i = 0; i < rels.length; i++) { 22983 var pos = calculateRelativePosition(this, elm, rels[i]); 22984 22985 if (this._fixed) { 22986 if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) { 22987 return rels[i]; 22988 } 22989 } else { 22990 if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x && 22991 pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) { 22992 return rels[i]; 22993 } 22994 } 22995 } 22996 22997 return rels[0]; 22998 }, 22999 23000 /** 23001 * Move relative to the specified element. 23002 * 23003 * @method moveRel 23004 * @param {Element} elm Element to move relative to. 23005 * @param {String} rel Relative mode. For example: br-tl. 23006 * @return {tinymce.ui.Control} Current control instance. 23007 */ 23008 moveRel: function(elm, rel) { 23009 if (typeof(rel) != 'string') { 23010 rel = this.testMoveRel(elm, rel); 23011 } 23012 23013 var pos = calculateRelativePosition(this, elm, rel); 23014 return this.moveTo(pos.x, pos.y); 23015 }, 23016 23017 /** 23018 * Move by a relative x, y values. 23019 * 23020 * @method moveBy 23021 * @param {Number} dx Relative x position. 23022 * @param {Number} dy Relative y position. 23023 * @return {tinymce.ui.Control} Current control instance. 23024 */ 23025 moveBy: function(dx, dy) { 23026 var self = this, rect = self.layoutRect(); 23027 23028 self.moveTo(rect.x + dx, rect.y + dy); 23029 23030 return self; 23031 }, 23032 23033 /** 23034 * Move to absolute position. 23035 * 23036 * @method moveTo 23037 * @param {Number} x Absolute x position. 23038 * @param {Number} y Absolute y position. 23039 * @return {tinymce.ui.Control} Current control instance. 23040 */ 23041 moveTo: function(x, y) { 23042 var self = this; 23043 23044 // TODO: Move this to some global class 23045 function contrain(value, max, size) { 23046 if (value < 0) { 23047 return 0; 23048 } 23049 23050 if (value + size > max) { 23051 value = max - size; 23052 return value < 0 ? 0 : value; 23053 } 23054 23055 return value; 23056 } 23057 23058 if (self.settings.constrainToViewport) { 23059 var viewPortRect = DomUtils.getViewPort(window); 23060 var layoutRect = self.layoutRect(); 23061 23062 x = contrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w); 23063 y = contrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h); 23064 } 23065 23066 if (self._rendered) { 23067 self.layoutRect({x: x, y: y}).repaint(); 23068 } else { 23069 self.settings.x = x; 23070 self.settings.y = y; 23071 } 23072 23073 self.fire('move', {x: x, y: y}); 23074 23075 return self; 23076 } 23077 }; 23078 }); 23079 23080 // Included from: js/tinymce/classes/ui/Resizable.js 23081 23082 /** 23083 * Resizable.js 23084 * 23085 * Copyright, Moxiecode Systems AB 23086 * Released under LGPL License. 23087 * 23088 * License: http://www.tinymce.com/license 23089 * Contributing: http://www.tinymce.com/contributing 23090 */ 23091 23092 /** 23093 * Resizable mixin. Enables controls to be resized. 23094 * 23095 * @mixin tinymce.ui.Resizable 23096 */ 23097 define("tinymce/ui/Resizable", [ 23098 "tinymce/ui/DomUtils" 23099 ], function(DomUtils) { 23100 "use strict"; 23101 23102 return { 23103 /** 23104 * Resizes the control to contents. 23105 * 23106 * @method resizeToContent 23107 */ 23108 resizeToContent: function() { 23109 this._layoutRect.autoResize = true; 23110 this._lastRect = null; 23111 this.reflow(); 23112 }, 23113 23114 /** 23115 * Resizes the control to a specific width/height. 23116 * 23117 * @method resizeTo 23118 * @param {Number} w Control width. 23119 * @param {Number} h Control height. 23120 * @return {tinymce.ui.Control} Current control instance. 23121 */ 23122 resizeTo: function(w, h) { 23123 // TODO: Fix hack 23124 if (w <= 1 || h <= 1) { 23125 var rect = DomUtils.getWindowSize(); 23126 23127 w = w <= 1 ? w * rect.w : w; 23128 h = h <= 1 ? h * rect.h : h; 23129 } 23130 23131 this._layoutRect.autoResize = false; 23132 return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow(); 23133 }, 23134 23135 /** 23136 * Resizes the control to a specific relative width/height. 23137 * 23138 * @method resizeBy 23139 * @param {Number} dw Relative control width. 23140 * @param {Number} dh Relative control height. 23141 * @return {tinymce.ui.Control} Current control instance. 23142 */ 23143 resizeBy: function(dw, dh) { 23144 var self = this, rect = self.layoutRect(); 23145 23146 return self.resizeTo(rect.w + dw, rect.h + dh); 23147 } 23148 }; 23149 }); 23150 23151 // Included from: js/tinymce/classes/ui/FloatPanel.js 23152 23153 /** 23154 * FloatPanel.js 23155 * 23156 * Copyright, Moxiecode Systems AB 23157 * Released under LGPL License. 23158 * 23159 * License: http://www.tinymce.com/license 23160 * Contributing: http://www.tinymce.com/contributing 23161 */ 23162 23163 /** 23164 * This class creates a floating panel. 23165 * 23166 * @-x-less FloatPanel.less 23167 * @class tinymce.ui.FloatPanel 23168 * @extends tinymce.ui.Panel 23169 * @mixes tinymce.ui.Movable 23170 * @mixes tinymce.ui.Resizable 23171 */ 23172 define("tinymce/ui/FloatPanel", [ 23173 "tinymce/ui/Panel", 23174 "tinymce/ui/Movable", 23175 "tinymce/ui/Resizable", 23176 "tinymce/ui/DomUtils" 23177 ], function(Panel, Movable, Resizable, DomUtils) { 23178 "use strict"; 23179 23180 var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = []; 23181 var zOrder = [], hasModal; 23182 23183 function bindDocumentClickHandler() { 23184 function isChildOf(ctrl, parent) { 23185 while (ctrl) { 23186 if (ctrl == parent) { 23187 return true; 23188 } 23189 23190 ctrl = ctrl.parent(); 23191 } 23192 } 23193 23194 if (!documentClickHandler) { 23195 documentClickHandler = function(e) { 23196 // Gecko fires click event and in the wrong order on Mac so lets normalize 23197 if (e.button == 2) { 23198 return; 23199 } 23200 23201 // Hide any float panel when a click is out side that float panel and the 23202 // float panels direct parent for example a click on a menu button 23203 var i = visiblePanels.length; 23204 while (i--) { 23205 var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); 23206 23207 if (panel.settings.autohide) { 23208 if (clickCtrl) { 23209 if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { 23210 continue; 23211 } 23212 } 23213 23214 e = panel.fire('autohide', {target: e.target}); 23215 if (!e.isDefaultPrevented()) { 23216 panel.hide(); 23217 } 23218 } 23219 } 23220 }; 23221 23222 DomUtils.on(document, 'click', documentClickHandler); 23223 } 23224 } 23225 23226 function bindDocumentScrollHandler() { 23227 if (!documentScrollHandler) { 23228 documentScrollHandler = function() { 23229 var i; 23230 23231 i = visiblePanels.length; 23232 while (i--) { 23233 repositionPanel(visiblePanels[i]); 23234 } 23235 }; 23236 23237 DomUtils.on(window, 'scroll', documentScrollHandler); 23238 } 23239 } 23240 23241 function bindWindowResizeHandler() { 23242 if (!windowResizeHandler) { 23243 var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight; 23244 23245 windowResizeHandler = function() { 23246 // Workaround for #7065 IE 7 fires resize events event though the window wasn't resized 23247 if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) { 23248 clientWidth = docElm.clientWidth; 23249 clientHeight = docElm.clientHeight; 23250 FloatPanel.hideAll(); 23251 } 23252 }; 23253 23254 DomUtils.on(window, 'resize', windowResizeHandler); 23255 } 23256 } 23257 23258 /** 23259 * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will 23260 * also reposition all child panels of the current panel. 23261 */ 23262 function repositionPanel(panel) { 23263 var scrollY = DomUtils.getViewPort().y; 23264 23265 function toggleFixedChildPanels(fixed, deltaY) { 23266 var parent; 23267 23268 for (var i = 0; i < visiblePanels.length; i++) { 23269 if (visiblePanels[i] != panel) { 23270 parent = visiblePanels[i].parent(); 23271 23272 while (parent && (parent = parent.parent())) { 23273 if (parent == panel) { 23274 visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); 23275 } 23276 } 23277 } 23278 } 23279 } 23280 23281 if (panel.settings.autofix) { 23282 if (!panel._fixed) { 23283 panel._autoFixY = panel.layoutRect().y; 23284 23285 if (panel._autoFixY < scrollY) { 23286 panel.fixed(true).layoutRect({y: 0}).repaint(); 23287 toggleFixedChildPanels(true, scrollY - panel._autoFixY); 23288 } 23289 } else { 23290 if (panel._autoFixY > scrollY) { 23291 panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint(); 23292 toggleFixedChildPanels(false, panel._autoFixY - scrollY); 23293 } 23294 } 23295 } 23296 } 23297 23298 function addRemove(add, ctrl) { 23299 var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal; 23300 23301 if (add) { 23302 zOrder.push(ctrl); 23303 } else { 23304 i = zOrder.length; 23305 23306 while (i--) { 23307 if (zOrder[i] === ctrl) { 23308 zOrder.splice(i, 1); 23309 } 23310 } 23311 } 23312 23313 if (zOrder.length) { 23314 for (i = 0; i < zOrder.length; i++) { 23315 if (zOrder[i].modal) { 23316 zIndex++; 23317 topModal = zOrder[i]; 23318 } 23319 23320 zOrder[i].getEl().style.zIndex = zIndex; 23321 zOrder[i].zIndex = zIndex; 23322 zIndex++; 23323 } 23324 } 23325 23326 var modalBlockEl = document.getElementById(ctrl.classPrefix + 'modal-block'); 23327 23328 if (topModal) { 23329 DomUtils.css(modalBlockEl, 'z-index', topModal.zIndex - 1); 23330 } else if (modalBlockEl) { 23331 modalBlockEl.parentNode.removeChild(modalBlockEl); 23332 hasModal = false; 23333 } 23334 23335 FloatPanel.currentZIndex = zIndex; 23336 } 23337 23338 var FloatPanel = Panel.extend({ 23339 Mixins: [Movable, Resizable], 23340 Defaults: { 23341 wrapFocus: true 23342 }, 23343 23344 /** 23345 * Constructs a new control instance with the specified settings. 23346 * 23347 * @constructor 23348 * @param {Object} settings Name/value object with settings. 23349 * @setting {Boolean} autohide Automatically hide the panel. 23350 */ 23351 init: function(settings) { 23352 var self = this; 23353 23354 self._super(settings); 23355 self._eventsRoot = self; 23356 23357 self.addClass('floatpanel'); 23358 23359 // Hide floatpanes on click out side the root button 23360 if (settings.autohide) { 23361 bindDocumentClickHandler(); 23362 bindWindowResizeHandler(); 23363 visiblePanels.push(self); 23364 } 23365 23366 if (settings.autofix) { 23367 bindDocumentScrollHandler(); 23368 23369 self.on('move', function() { 23370 repositionPanel(this); 23371 }); 23372 } 23373 23374 self.on('postrender show', function(e) { 23375 if (e.control == self) { 23376 var modalBlockEl, prefix = self.classPrefix; 23377 23378 if (self.modal && !hasModal) { 23379 modalBlockEl = DomUtils.createFragment('<div id="' + prefix + 'modal-block" class="' + 23380 prefix + 'reset ' + prefix + 'fade"></div>'); 23381 modalBlockEl = modalBlockEl.firstChild; 23382 23383 self.getContainerElm().appendChild(modalBlockEl); 23384 23385 setTimeout(function() { 23386 DomUtils.addClass(modalBlockEl, prefix + 'in'); 23387 DomUtils.addClass(self.getEl(), prefix + 'in'); 23388 }, 0); 23389 23390 hasModal = true; 23391 } 23392 23393 addRemove(true, self); 23394 } 23395 }); 23396 23397 self.on('show', function() { 23398 self.parents().each(function(ctrl) { 23399 if (ctrl._fixed) { 23400 self.fixed(true); 23401 return false; 23402 } 23403 }); 23404 }); 23405 23406 if (settings.popover) { 23407 self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>'; 23408 self.addClass('popover').addClass('bottom').addClass(self.isRtl() ? 'end' : 'start'); 23409 } 23410 }, 23411 23412 fixed: function(state) { 23413 var self = this; 23414 23415 if (self._fixed != state) { 23416 if (self._rendered) { 23417 var viewport = DomUtils.getViewPort(); 23418 23419 if (state) { 23420 self.layoutRect().y -= viewport.y; 23421 } else { 23422 self.layoutRect().y += viewport.y; 23423 } 23424 } 23425 23426 self.toggleClass('fixed', state); 23427 self._fixed = state; 23428 } 23429 23430 return self; 23431 }, 23432 23433 /** 23434 * Shows the current float panel. 23435 * 23436 * @method show 23437 * @return {tinymce.ui.FloatPanel} Current floatpanel instance. 23438 */ 23439 show: function() { 23440 var self = this, i, state = self._super(); 23441 23442 i = visiblePanels.length; 23443 while (i--) { 23444 if (visiblePanels[i] === self) { 23445 break; 23446 } 23447 } 23448 23449 if (i === -1) { 23450 visiblePanels.push(self); 23451 } 23452 23453 return state; 23454 }, 23455 23456 /** 23457 * Hides the current float panel. 23458 * 23459 * @method hide 23460 * @return {tinymce.ui.FloatPanel} Current floatpanel instance. 23461 */ 23462 hide: function() { 23463 removeVisiblePanel(this); 23464 addRemove(false, this); 23465 23466 return this._super(); 23467 }, 23468 23469 /** 23470 * Hide all visible float panels with he autohide setting enabled. This is for 23471 * manually hiding floating menus or panels. 23472 * 23473 * @method hideAll 23474 */ 23475 hideAll: function() { 23476 FloatPanel.hideAll(); 23477 }, 23478 23479 /** 23480 * Closes the float panel. This will remove the float panel from page and fire the close event. 23481 * 23482 * @method close 23483 */ 23484 close: function() { 23485 var self = this; 23486 23487 if (!self.fire('close').isDefaultPrevented()) { 23488 self.remove(); 23489 addRemove(false, self); 23490 } 23491 23492 return self; 23493 }, 23494 23495 /** 23496 * Removes the float panel from page. 23497 * 23498 * @method remove 23499 */ 23500 remove: function() { 23501 removeVisiblePanel(this); 23502 this._super(); 23503 }, 23504 23505 postRender: function() { 23506 var self = this; 23507 23508 if (self.settings.bodyRole) { 23509 this.getEl('body').setAttribute('role', self.settings.bodyRole); 23510 } 23511 23512 return self._super(); 23513 } 23514 }); 23515 23516 /** 23517 * Hide all visible float panels with he autohide setting enabled. This is for 23518 * manually hiding floating menus or panels. 23519 * 23520 * @static 23521 * @method hideAll 23522 */ 23523 FloatPanel.hideAll = function() { 23524 var i = visiblePanels.length; 23525 23526 while (i--) { 23527 var panel = visiblePanels[i]; 23528 23529 if (panel && panel.settings.autohide) { 23530 panel.hide(); 23531 visiblePanels.splice(i, 1); 23532 } 23533 } 23534 }; 23535 23536 function removeVisiblePanel(panel) { 23537 var i; 23538 23539 i = visiblePanels.length; 23540 while (i--) { 23541 if (visiblePanels[i] === panel) { 23542 visiblePanels.splice(i, 1); 23543 } 23544 } 23545 23546 i = zOrder.length; 23547 while (i--) { 23548 if (zOrder[i] === panel) { 23549 zOrder.splice(i, 1); 23550 } 23551 } 23552 } 23553 23554 return FloatPanel; 23555 }); 23556 23557 // Included from: js/tinymce/classes/ui/Window.js 23558 23559 /** 23560 * Window.js 23561 * 23562 * Copyright, Moxiecode Systems AB 23563 * Released under LGPL License. 23564 * 23565 * License: http://www.tinymce.com/license 23566 * Contributing: http://www.tinymce.com/contributing 23567 */ 23568 23569 /** 23570 * Creates a new window. 23571 * 23572 * @-x-less Window.less 23573 * @class tinymce.ui.Window 23574 * @extends tinymce.ui.FloatPanel 23575 */ 23576 define("tinymce/ui/Window", [ 23577 "tinymce/ui/FloatPanel", 23578 "tinymce/ui/Panel", 23579 "tinymce/ui/DomUtils", 23580 "tinymce/ui/DragHelper" 23581 ], function(FloatPanel, Panel, DomUtils, DragHelper) { 23582 "use strict"; 23583 23584 var Window = FloatPanel.extend({ 23585 modal: true, 23586 23587 Defaults: { 23588 border: 1, 23589 layout: 'flex', 23590 containerCls: 'panel', 23591 role: 'dialog', 23592 ariaRoot: true, 23593 callbacks: { 23594 submit: function() { 23595 this.fire('submit', {data: this.toJSON()}); 23596 }, 23597 23598 close: function() { 23599 this.close(); 23600 } 23601 } 23602 }, 23603 23604 /** 23605 * Constructs a instance with the specified settings. 23606 * 23607 * @constructor 23608 * @param {Object} settings Name/value object with settings. 23609 */ 23610 init: function(settings) { 23611 var self = this; 23612 23613 self._super(settings); 23614 23615 if (self.isRtl()) { 23616 self.addClass('rtl'); 23617 } 23618 23619 self.addClass('window'); 23620 self._fixed = true; 23621 23622 // Create statusbar 23623 if (settings.buttons) { 23624 self.statusbar = new Panel({ 23625 layout: 'flex', 23626 border: '1 0 0 0', 23627 spacing: 3, 23628 padding: 10, 23629 align: 'center', 23630 pack: self.isRtl() ? 'start' : 'end', 23631 defaults: { 23632 type: 'button' 23633 }, 23634 items: settings.buttons 23635 }); 23636 23637 self.statusbar.addClass('foot'); 23638 self.statusbar.parent(self); 23639 } 23640 23641 self.on('click', function(e) { 23642 if (e.target.className.indexOf(self.classPrefix + 'close') != -1) { 23643 self.close(); 23644 } 23645 }); 23646 23647 self.on('cancel', function(e) { 23648 e.preventDefault(); 23649 e.stopPropagation(); 23650 self.close(); 23651 }); 23652 23653 self.aria('describedby', self.describedBy || self._id + '-title'); 23654 self.aria('label', settings.title); 23655 self._fullscreen = false; 23656 }, 23657 23658 /** 23659 * Recalculates the positions of the controls in the current container. 23660 * This is invoked by the reflow method and shouldn't be called directly. 23661 * 23662 * @method recalc 23663 */ 23664 recalc: function() { 23665 var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc; 23666 23667 if (self._fullscreen) { 23668 self.layoutRect(DomUtils.getWindowSize()); 23669 self.layoutRect().contentH = self.layoutRect().innerH; 23670 } 23671 23672 self._super(); 23673 23674 layoutRect = self.layoutRect(); 23675 23676 // Resize window based on title width 23677 if (self.settings.title && !self._fullscreen) { 23678 width = layoutRect.headerW; 23679 if (width > layoutRect.w) { 23680 x = layoutRect.x - Math.max(0, width / 2); 23681 self.layoutRect({w: width, x: x}); 23682 needsRecalc = true; 23683 } 23684 } 23685 23686 // Resize window based on statusbar width 23687 if (statusbar) { 23688 statusbar.layoutRect({w: self.layoutRect().innerW}).recalc(); 23689 23690 width = statusbar.layoutRect().minW + layoutRect.deltaW; 23691 if (width > layoutRect.w) { 23692 x = layoutRect.x - Math.max(0, width - layoutRect.w); 23693 self.layoutRect({w: width, x: x}); 23694 needsRecalc = true; 23695 } 23696 } 23697 23698 // Recalc body and disable auto resize 23699 if (needsRecalc) { 23700 self.recalc(); 23701 } 23702 }, 23703 23704 /** 23705 * Initializes the current controls layout rect. 23706 * This will be executed by the layout managers to determine the 23707 * default minWidth/minHeight etc. 23708 * 23709 * @method initLayoutRect 23710 * @return {Object} Layout rect instance. 23711 */ 23712 initLayoutRect: function() { 23713 var self = this, layoutRect = self._super(), deltaH = 0, headEl; 23714 23715 // Reserve vertical space for title 23716 if (self.settings.title && !self._fullscreen) { 23717 headEl = self.getEl('head'); 23718 23719 var size = DomUtils.getSize(headEl); 23720 23721 layoutRect.headerW = size.width; 23722 layoutRect.headerH = size.height; 23723 23724 deltaH += layoutRect.headerH; 23725 } 23726 23727 // Reserve vertical space for statusbar 23728 if (self.statusbar) { 23729 deltaH += self.statusbar.layoutRect().h; 23730 } 23731 23732 layoutRect.deltaH += deltaH; 23733 layoutRect.minH += deltaH; 23734 //layoutRect.innerH -= deltaH; 23735 layoutRect.h += deltaH; 23736 23737 var rect = DomUtils.getWindowSize(); 23738 23739 layoutRect.x = Math.max(0, rect.w / 2 - layoutRect.w / 2); 23740 layoutRect.y = Math.max(0, rect.h / 2 - layoutRect.h / 2); 23741 23742 return layoutRect; 23743 }, 23744 23745 /** 23746 * Renders the control as a HTML string. 23747 * 23748 * @method renderHtml 23749 * @return {String} HTML representing the control. 23750 */ 23751 renderHtml: function() { 23752 var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix; 23753 var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html; 23754 23755 self.preRender(); 23756 layout.preRender(self); 23757 23758 if (settings.title) { 23759 headerHtml = ( 23760 '<div id="' + id + '-head" class="' + prefix + 'window-head">' + 23761 '<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' + 23762 '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>' + 23763 '<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' + 23764 '</div>' 23765 ); 23766 } 23767 23768 if (settings.url) { 23769 html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>'; 23770 } 23771 23772 if (typeof(html) == "undefined") { 23773 html = layout.renderHtml(self); 23774 } 23775 23776 if (self.statusbar) { 23777 footerHtml = self.statusbar.renderHtml(); 23778 } 23779 23780 return ( 23781 '<div id="' + id + '" class="' + self.classes() + '" hidefocus="1">' + 23782 '<div class="' + self.classPrefix + 'reset" role="application">' + 23783 headerHtml + 23784 '<div id="' + id + '-body" class="' + self.classes('body') + '">' + 23785 html + 23786 '</div>' + 23787 footerHtml + 23788 '</div>' + 23789 '</div>' 23790 ); 23791 }, 23792 23793 /** 23794 * Switches the window fullscreen mode. 23795 * 23796 * @method fullscreen 23797 * @param {Boolean} state True/false state. 23798 * @return {tinymce.ui.Window} Current window instance. 23799 */ 23800 fullscreen: function(state) { 23801 var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect; 23802 23803 if (state != self._fullscreen) { 23804 DomUtils.on(window, 'resize', function() { 23805 var time; 23806 23807 if (self._fullscreen) { 23808 // Time the layout time if it's to slow use a timeout to not hog the CPU 23809 if (!slowRendering) { 23810 time = new Date().getTime(); 23811 23812 var rect = DomUtils.getWindowSize(); 23813 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 23814 23815 if ((new Date().getTime()) - time > 50) { 23816 slowRendering = true; 23817 } 23818 } else { 23819 if (!self._timer) { 23820 self._timer = setTimeout(function() { 23821 var rect = DomUtils.getWindowSize(); 23822 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 23823 23824 self._timer = 0; 23825 }, 50); 23826 } 23827 } 23828 } 23829 }); 23830 23831 layoutRect = self.layoutRect(); 23832 self._fullscreen = state; 23833 23834 if (!state) { 23835 self._borderBox = self.parseBox(self.settings.border); 23836 self.getEl('head').style.display = ''; 23837 layoutRect.deltaH += layoutRect.headerH; 23838 DomUtils.removeClass(documentElement, prefix + 'fullscreen'); 23839 DomUtils.removeClass(document.body, prefix + 'fullscreen'); 23840 self.removeClass('fullscreen'); 23841 self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h); 23842 } else { 23843 self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h}; 23844 23845 self._borderBox = self.parseBox('0'); 23846 self.getEl('head').style.display = 'none'; 23847 layoutRect.deltaH -= layoutRect.headerH + 2; 23848 DomUtils.addClass(documentElement, prefix + 'fullscreen'); 23849 DomUtils.addClass(document.body, prefix + 'fullscreen'); 23850 self.addClass('fullscreen'); 23851 23852 var rect = DomUtils.getWindowSize(); 23853 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 23854 } 23855 } 23856 23857 return self.reflow(); 23858 }, 23859 23860 /** 23861 * Called after the control has been rendered. 23862 * 23863 * @method postRender 23864 */ 23865 postRender: function() { 23866 var self = this, startPos; 23867 23868 setTimeout(function() { 23869 self.addClass('in'); 23870 }, 0); 23871 23872 self._super(); 23873 23874 if (self.statusbar) { 23875 self.statusbar.postRender(); 23876 } 23877 23878 self.focus(); 23879 23880 this.dragHelper = new DragHelper(self._id + '-dragh', { 23881 start: function() { 23882 startPos = { 23883 x: self.layoutRect().x, 23884 y: self.layoutRect().y 23885 }; 23886 }, 23887 23888 drag: function(e) { 23889 self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY); 23890 } 23891 }); 23892 23893 self.on('submit', function(e) { 23894 if (!e.isDefaultPrevented()) { 23895 self.close(); 23896 } 23897 }); 23898 }, 23899 23900 /** 23901 * Fires a submit event with the serialized form. 23902 * 23903 * @method submit 23904 * @return {Object} Event arguments object. 23905 */ 23906 submit: function() { 23907 return this.fire('submit', {data: this.toJSON()}); 23908 }, 23909 23910 /** 23911 * Removes the current control from DOM and from UI collections. 23912 * 23913 * @method remove 23914 * @return {tinymce.ui.Control} Current control instance. 23915 */ 23916 remove: function() { 23917 var self = this, prefix = self.classPrefix; 23918 23919 self.dragHelper.destroy(); 23920 self._super(); 23921 23922 if (self.statusbar) { 23923 this.statusbar.remove(); 23924 } 23925 23926 if (self._fullscreen) { 23927 DomUtils.removeClass(document.documentElement, prefix + 'fullscreen'); 23928 DomUtils.removeClass(document.body, prefix + 'fullscreen'); 23929 } 23930 }, 23931 23932 /** 23933 * Returns the contentWindow object of the iframe if it exists. 23934 * 23935 * @method getContentWindow 23936 * @return {Window} window object or null. 23937 */ 23938 getContentWindow: function() { 23939 var ifr = this.getEl().getElementsByTagName('iframe')[0]; 23940 return ifr ? ifr.contentWindow : null; 23941 } 23942 }); 23943 23944 return Window; 23945 }); 23946 23947 // Included from: js/tinymce/classes/ui/MessageBox.js 23948 23949 /** 23950 * MessageBox.js 23951 * 23952 * Copyright, Moxiecode Systems AB 23953 * Released under LGPL License. 23954 * 23955 * License: http://www.tinymce.com/license 23956 * Contributing: http://www.tinymce.com/contributing 23957 */ 23958 23959 /** 23960 * This class is used to create MessageBoxes like alerts/confirms etc. 23961 * 23962 * @class tinymce.ui.MessageBox 23963 * @extends tinymce.ui.Window 23964 */ 23965 define("tinymce/ui/MessageBox", [ 23966 "tinymce/ui/Window" 23967 ], function(Window) { 23968 "use strict"; 23969 23970 var MessageBox = Window.extend({ 23971 /** 23972 * Constructs a instance with the specified settings. 23973 * 23974 * @constructor 23975 * @param {Object} settings Name/value object with settings. 23976 */ 23977 init: function(settings) { 23978 settings = { 23979 border: 1, 23980 padding: 20, 23981 layout: 'flex', 23982 pack: "center", 23983 align: "center", 23984 containerCls: 'panel', 23985 autoScroll: true, 23986 buttons: {type: "button", text: "Ok", action: "ok"}, 23987 items: { 23988 type: "label", 23989 multiline: true, 23990 maxWidth: 500, 23991 maxHeight: 200 23992 } 23993 }; 23994 23995 this._super(settings); 23996 }, 23997 23998 Statics: { 23999 /** 24000 * Ok buttons constant. 24001 * 24002 * @static 24003 * @final 24004 * @field {Number} OK 24005 */ 24006 OK: 1, 24007 24008 /** 24009 * Ok/cancel buttons constant. 24010 * 24011 * @static 24012 * @final 24013 * @field {Number} OK_CANCEL 24014 */ 24015 OK_CANCEL: 2, 24016 24017 /** 24018 * yes/no buttons constant. 24019 * 24020 * @static 24021 * @final 24022 * @field {Number} YES_NO 24023 */ 24024 YES_NO: 3, 24025 24026 /** 24027 * yes/no/cancel buttons constant. 24028 * 24029 * @static 24030 * @final 24031 * @field {Number} YES_NO_CANCEL 24032 */ 24033 YES_NO_CANCEL: 4, 24034 24035 /** 24036 * Constructs a new message box and renders it to the body element. 24037 * 24038 * @static 24039 * @method msgBox 24040 * @param {Object} settings Name/value object with settings. 24041 */ 24042 msgBox: function(settings) { 24043 var buttons, callback = settings.callback || function() {}; 24044 24045 function createButton(text, status, primary) { 24046 return { 24047 type: "button", 24048 text: text, 24049 subtype: primary ? 'primary' : '', 24050 onClick: function(e) { 24051 e.control.parents()[1].close(); 24052 callback(status); 24053 } 24054 }; 24055 } 24056 24057 switch (settings.buttons) { 24058 case MessageBox.OK_CANCEL: 24059 buttons = [ 24060 createButton('Ok', true, true), 24061 createButton('Cancel', false) 24062 ]; 24063 break; 24064 24065 case MessageBox.YES_NO: 24066 case MessageBox.YES_NO_CANCEL: 24067 buttons = [ 24068 createButton('Yes', 1, true), 24069 createButton('No', 0) 24070 ]; 24071 24072 if (settings.buttons == MessageBox.YES_NO_CANCEL) { 24073 buttons.push(createButton('Cancel', -1)); 24074 } 24075 break; 24076 24077 default: 24078 buttons = [ 24079 createButton('Ok', true, true) 24080 ]; 24081 break; 24082 } 24083 24084 return new Window({ 24085 padding: 20, 24086 x: settings.x, 24087 y: settings.y, 24088 minWidth: 300, 24089 minHeight: 100, 24090 layout: "flex", 24091 pack: "center", 24092 align: "center", 24093 buttons: buttons, 24094 title: settings.title, 24095 role: 'alertdialog', 24096 items: { 24097 type: "label", 24098 multiline: true, 24099 maxWidth: 500, 24100 maxHeight: 200, 24101 text: settings.text 24102 }, 24103 onPostRender: function() { 24104 this.aria('describedby', this.items()[0]._id); 24105 }, 24106 onClose: settings.onClose, 24107 onCancel: function() { 24108 callback(false); 24109 } 24110 }).renderTo(document.body).reflow(); 24111 }, 24112 24113 /** 24114 * Creates a new alert dialog. 24115 * 24116 * @method alert 24117 * @param {Object} settings Settings for the alert dialog. 24118 * @param {function} [callback] Callback to execute when the user makes a choice. 24119 */ 24120 alert: function(settings, callback) { 24121 if (typeof(settings) == "string") { 24122 settings = {text: settings}; 24123 } 24124 24125 settings.callback = callback; 24126 return MessageBox.msgBox(settings); 24127 }, 24128 24129 /** 24130 * Creates a new confirm dialog. 24131 * 24132 * @method confirm 24133 * @param {Object} settings Settings for the confirm dialog. 24134 * @param {function} [callback] Callback to execute when the user makes a choice. 24135 */ 24136 confirm: function(settings, callback) { 24137 if (typeof(settings) == "string") { 24138 settings = {text: settings}; 24139 } 24140 24141 settings.callback = callback; 24142 settings.buttons = MessageBox.OK_CANCEL; 24143 24144 return MessageBox.msgBox(settings); 24145 } 24146 } 24147 }); 24148 24149 return MessageBox; 24150 }); 24151 24152 // Included from: js/tinymce/classes/WindowManager.js 24153 24154 /** 24155 * WindowManager.js 24156 * 24157 * Copyright, Moxiecode Systems AB 24158 * Released under LGPL License. 24159 * 24160 * License: http://www.tinymce.com/license 24161 * Contributing: http://www.tinymce.com/contributing 24162 */ 24163 24164 /** 24165 * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs. 24166 * 24167 * @class tinymce.WindowManager 24168 * @example 24169 * // Opens a new dialog with the file.htm file and the size 320x240 24170 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. 24171 * tinymce.activeEditor.windowManager.open({ 24172 * url: 'file.htm', 24173 * width: 320, 24174 * height: 240 24175 * }, { 24176 * custom_param: 1 24177 * }); 24178 * 24179 * // Displays an alert box using the active editors window manager instance 24180 * tinymce.activeEditor.windowManager.alert('Hello world!'); 24181 * 24182 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm 24183 * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { 24184 * if (s) 24185 * tinymce.activeEditor.windowManager.alert("Ok"); 24186 * else 24187 * tinymce.activeEditor.windowManager.alert("Cancel"); 24188 * }); 24189 */ 24190 define("tinymce/WindowManager", [ 24191 "tinymce/ui/Window", 24192 "tinymce/ui/MessageBox" 24193 ], function(Window, MessageBox) { 24194 return function(editor) { 24195 var self = this, windows = []; 24196 24197 function getTopMostWindow() { 24198 if (windows.length) { 24199 return windows[windows.length - 1]; 24200 } 24201 } 24202 24203 self.windows = windows; 24204 24205 editor.on('remove', function() { 24206 var i = windows.length; 24207 24208 while (i--) { 24209 windows[i].close(); 24210 } 24211 }); 24212 24213 /** 24214 * Opens a new window. 24215 * 24216 * @method open 24217 * @param {Object} args Optional name/value settings collection contains things like width/height/url etc. 24218 * @option {String} title Window title. 24219 * @option {String} file URL of the file to open in the window. 24220 * @option {Number} width Width in pixels. 24221 * @option {Number} height Height in pixels. 24222 * @option {Boolean} resizable Specifies whether the popup window is resizable or not. 24223 * @option {Boolean} maximizable Specifies whether the popup window has a "maximize" button and can get maximized or not. 24224 * @option {String/Boolean} scrollbars Specifies whether the popup window can have scrollbars if required (i.e. content 24225 * larger than the popup size specified). 24226 */ 24227 self.open = function(args, params) { 24228 var win; 24229 24230 editor.editorManager.setActive(editor); 24231 24232 args.title = args.title || ' '; 24233 24234 // Handle URL 24235 args.url = args.url || args.file; // Legacy 24236 if (args.url) { 24237 args.width = parseInt(args.width || 320, 10); 24238 args.height = parseInt(args.height || 240, 10); 24239 } 24240 24241 // Handle body 24242 if (args.body) { 24243 args.items = { 24244 defaults: args.defaults, 24245 type: args.bodyType || 'form', 24246 items: args.body 24247 }; 24248 } 24249 24250 if (!args.url && !args.buttons) { 24251 args.buttons = [ 24252 {text: 'Ok', subtype: 'primary', onclick: function() { 24253 win.find('form')[0].submit(); 24254 }}, 24255 24256 {text: 'Cancel', onclick: function() { 24257 win.close(); 24258 }} 24259 ]; 24260 } 24261 24262 win = new Window(args); 24263 windows.push(win); 24264 24265 win.on('close', function() { 24266 var i = windows.length; 24267 24268 while (i--) { 24269 if (windows[i] === win) { 24270 windows.splice(i, 1); 24271 } 24272 } 24273 24274 if (!windows.length) { 24275 editor.focus(); 24276 } 24277 }); 24278 24279 // Handle data 24280 if (args.data) { 24281 win.on('postRender', function() { 24282 this.find('*').each(function(ctrl) { 24283 var name = ctrl.name(); 24284 24285 if (name in args.data) { 24286 ctrl.value(args.data[name]); 24287 } 24288 }); 24289 }); 24290 } 24291 24292 // store args and parameters 24293 win.features = args || {}; 24294 win.params = params || {}; 24295 24296 // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog 24297 if (windows.length === 1) { 24298 editor.nodeChanged(); 24299 } 24300 24301 return win.renderTo().reflow(); 24302 }; 24303 24304 /** 24305 * Creates a alert dialog. Please don't use the blocking behavior of this 24306 * native version use the callback method instead then it can be extended. 24307 * 24308 * @method alert 24309 * @param {String} message Text to display in the new alert dialog. 24310 * @param {function} callback Callback function to be executed after the user has selected ok. 24311 * @param {Object} scope Optional scope to execute the callback in. 24312 * @example 24313 * // Displays an alert box using the active editors window manager instance 24314 * tinymce.activeEditor.windowManager.alert('Hello world!'); 24315 */ 24316 self.alert = function(message, callback, scope) { 24317 MessageBox.alert(message, function() { 24318 if (callback) { 24319 callback.call(scope || this); 24320 } else { 24321 editor.focus(); 24322 } 24323 }); 24324 }; 24325 24326 /** 24327 * Creates a confirm dialog. Please don't use the blocking behavior of this 24328 * native version use the callback method instead then it can be extended. 24329 * 24330 * @method confirm 24331 * @param {String} messageText to display in the new confirm dialog. 24332 * @param {function} callback Callback function to be executed after the user has selected ok or cancel. 24333 * @param {Object} scope Optional scope to execute the callback in. 24334 * @example 24335 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm 24336 * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { 24337 * if (s) 24338 * tinymce.activeEditor.windowManager.alert("Ok"); 24339 * else 24340 * tinymce.activeEditor.windowManager.alert("Cancel"); 24341 * }); 24342 */ 24343 self.confirm = function(message, callback, scope) { 24344 MessageBox.confirm(message, function(state) { 24345 callback.call(scope || this, state); 24346 }); 24347 }; 24348 24349 /** 24350 * Closes the top most window. 24351 * 24352 * @method close 24353 */ 24354 self.close = function() { 24355 if (getTopMostWindow()) { 24356 getTopMostWindow().close(); 24357 } 24358 }; 24359 24360 /** 24361 * Returns the params of the last window open call. This can be used in iframe based 24362 * dialog to get params passed from the tinymce plugin. 24363 * 24364 * @example 24365 * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams(); 24366 * 24367 * @method getParams 24368 * @return {Object} Name/value object with parameters passed from windowManager.open call. 24369 */ 24370 self.getParams = function() { 24371 return getTopMostWindow() ? getTopMostWindow().params : null; 24372 }; 24373 24374 /** 24375 * Sets the params of the last opened window. 24376 * 24377 * @method setParams 24378 * @param {Object} params Params object to set for the last opened window. 24379 */ 24380 self.setParams = function(params) { 24381 if (getTopMostWindow()) { 24382 getTopMostWindow().params = params; 24383 } 24384 }; 24385 24386 /** 24387 * Returns the currently opened window objects. 24388 * 24389 * @method getWindows 24390 * @return {Array} Array of the currently opened windows. 24391 */ 24392 self.getWindows = function() { 24393 return windows; 24394 }; 24395 }; 24396 }); 24397 24398 // Included from: js/tinymce/classes/util/Quirks.js 24399 24400 /** 24401 * Quirks.js 24402 * 24403 * Copyright, Moxiecode Systems AB 24404 * Released under LGPL License. 24405 * 24406 * License: http://www.tinymce.com/license 24407 * Contributing: http://www.tinymce.com/contributing 24408 * 24409 * @ignore-file 24410 */ 24411 24412 /** 24413 * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes. 24414 * 24415 * @class tinymce.util.Quirks 24416 */ 24417 define("tinymce/util/Quirks", [ 24418 "tinymce/util/VK", 24419 "tinymce/dom/RangeUtils", 24420 "tinymce/html/Node", 24421 "tinymce/html/Entities", 24422 "tinymce/Env", 24423 "tinymce/util/Tools" 24424 ], function(VK, RangeUtils, Node, Entities, Env, Tools) { 24425 return function(editor) { 24426 var each = Tools.each; 24427 var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, 24428 settings = editor.settings, parser = editor.parser, serializer = editor.serializer; 24429 var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit; 24430 24431 /** 24432 * Executes a command with a specific state this can be to enable/disable browser editing features. 24433 */ 24434 function setEditorCommandState(cmd, state) { 24435 try { 24436 editor.getDoc().execCommand(cmd, false, state); 24437 } catch (ex) { 24438 // Ignore 24439 } 24440 } 24441 24442 /** 24443 * Returns current IE document mode. 24444 */ 24445 function getDocumentMode() { 24446 var documentMode = editor.getDoc().documentMode; 24447 24448 return documentMode ? documentMode : 6; 24449 } 24450 24451 /** 24452 * Returns true/false if the event is prevented or not. 24453 * 24454 * @private 24455 * @param {Event} e Event object. 24456 * @return {Boolean} true/false if the event is prevented or not. 24457 */ 24458 function isDefaultPrevented(e) { 24459 return e.isDefaultPrevented(); 24460 } 24461 24462 /** 24463 * Fixes a WebKit bug when deleting contents using backspace or delete key. 24464 * WebKit will produce a span element if you delete across two block elements. 24465 * 24466 * Example: 24467 * <h1>a</h1><p>|b</p> 24468 * 24469 * Will produce this on backspace: 24470 * <h1>a<span style="<all runtime styles>">b</span></p> 24471 * 24472 * This fixes the backspace to produce: 24473 * <h1>a|b</p> 24474 * 24475 * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784 24476 * 24477 * This fixes the following delete scenarios: 24478 * 1. Delete by pressing backspace key. 24479 * 2. Delete by pressing delete key. 24480 * 3. Delete by pressing backspace key with ctrl/cmd (Word delete). 24481 * 4. Delete by pressing delete key with ctrl/cmd (Word delete). 24482 * 5. Delete by drag/dropping contents inside the editor. 24483 * 6. Delete by using Cut Ctrl+X/Cmd+X. 24484 * 7. Delete by selecting contents and writing a character.' 24485 * 24486 * This code is a ugly hack since writing full custom delete logic for just this bug 24487 * fix seemed like a huge task. I hope we can remove this before the year 2030. 24488 */ 24489 function cleanupStylesWhenDeleting() { 24490 var doc = editor.getDoc(), urlPrefix = 'data:text/mce-internal,'; 24491 var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng; 24492 24493 // Add mini polyfill for older WebKits 24494 // TODO: Remove this when old Safari versions gets updated 24495 if (!MutationObserver) { 24496 olderWebKit = true; 24497 24498 MutationObserver = function() { 24499 var records = [], target; 24500 24501 function nodeInsert(e) { 24502 var target = e.relatedNode || e.target; 24503 records.push({target: target, addedNodes: [target]}); 24504 } 24505 24506 function attrModified(e) { 24507 var target = e.relatedNode || e.target; 24508 records.push({target: target, attributeName: e.attrName}); 24509 } 24510 24511 this.observe = function(node) { 24512 target = node; 24513 target.addEventListener('DOMSubtreeModified', nodeInsert, false); 24514 target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); 24515 target.addEventListener('DOMNodeInserted', nodeInsert, false); 24516 target.addEventListener('DOMAttrModified', attrModified, false); 24517 }; 24518 24519 this.disconnect = function() { 24520 target.removeEventListener('DOMSubtreeModified', nodeInsert, false); 24521 target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); 24522 target.removeEventListener('DOMNodeInserted', nodeInsert, false); 24523 target.removeEventListener('DOMAttrModified', attrModified, false); 24524 }; 24525 24526 this.takeRecords = function() { 24527 return records; 24528 }; 24529 }; 24530 } 24531 24532 function customDelete(isForward) { 24533 var mutationObserver = new MutationObserver(function() {}); 24534 24535 Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) { 24536 // Mark existing spans 24537 if (elm.tagName == 'SPAN') { 24538 elm.setAttribute('mce-data-marked', 1); 24539 } 24540 24541 // Make sure all elements has a data-mce-style attribute 24542 if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) { 24543 editor.dom.setAttrib(elm, 'style', editor.dom.getAttrib(elm, 'style')); 24544 } 24545 }); 24546 24547 // Observe added nodes and style attribute changes 24548 mutationObserver.observe(editor.getDoc(), { 24549 childList: true, 24550 attributes: true, 24551 subtree: true, 24552 attributeFilter: ['style'] 24553 }); 24554 24555 editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null); 24556 24557 var rng = editor.selection.getRng(); 24558 var caretElement = rng.startContainer.parentNode; 24559 24560 Tools.each(mutationObserver.takeRecords(), function(record) { 24561 if (!dom.isChildOf(record.target, editor.getBody())) { 24562 return; 24563 } 24564 24565 // Restore style attribute to previous value 24566 if (record.attributeName == "style") { 24567 var oldValue = record.target.getAttribute('data-mce-style'); 24568 24569 if (oldValue) { 24570 record.target.setAttribute("style", oldValue); 24571 } else { 24572 record.target.removeAttribute("style"); 24573 } 24574 } 24575 24576 // Remove all spans that isn't maked and retain selection 24577 Tools.each(record.addedNodes, function(node) { 24578 if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) { 24579 var offset, container; 24580 24581 if (node == caretElement) { 24582 offset = rng.startOffset; 24583 container = node.firstChild; 24584 } 24585 24586 dom.remove(node, true); 24587 24588 if (container) { 24589 rng.setStart(container, offset); 24590 rng.setEnd(container, offset); 24591 editor.selection.setRng(rng); 24592 } 24593 } 24594 }); 24595 }); 24596 24597 mutationObserver.disconnect(); 24598 24599 // Remove any left over marks 24600 Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) { 24601 span.removeAttribute('mce-data-marked'); 24602 }); 24603 } 24604 24605 editor.on('keydown', function(e) { 24606 var isForward = e.keyCode == DELETE, isMeta = VK.metaKeyPressed(e); 24607 24608 if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) { 24609 var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset; 24610 24611 // Ignore non meta delete in the where there is text before/after the caret 24612 if (!isMeta && rng.collapsed && container.nodeType == 3) { 24613 if (isForward ? offset < container.data.length : offset > 0) { 24614 return; 24615 } 24616 } 24617 24618 e.preventDefault(); 24619 24620 if (isMeta) { 24621 editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", "word"); 24622 } 24623 24624 customDelete(isForward); 24625 } 24626 }); 24627 24628 editor.on('keypress', function(e) { 24629 if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode && !VK.metaKeyPressed(e)) { 24630 e.preventDefault(); 24631 customDelete(true); 24632 editor.selection.setContent(String.fromCharCode(e.charCode)); 24633 } 24634 }); 24635 24636 editor.addCommand('Delete', function() { 24637 customDelete(); 24638 }); 24639 24640 editor.addCommand('ForwardDelete', function() { 24641 customDelete(true); 24642 }); 24643 24644 // Older WebKits doesn't properly handle the clipboard so we can't add the rest 24645 if (olderWebKit) { 24646 return; 24647 } 24648 24649 editor.on('dragstart', function(e) { 24650 var selectionHtml; 24651 24652 if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') { 24653 selection.select(e.target); 24654 } 24655 24656 dragStartRng = selection.getRng(); 24657 selectionHtml = editor.selection.getContent(); 24658 24659 // Safari doesn't support custom dataTransfer items so we can only use URL and Text 24660 if (selectionHtml.length > 0) { 24661 e.dataTransfer.setData('URL', 'data:text/mce-internal,' + escape(selectionHtml)); 24662 } 24663 }); 24664 24665 editor.on('drop', function(e) { 24666 if (!isDefaultPrevented(e)) { 24667 var internalContent = e.dataTransfer.getData('URL'); 24668 24669 if (!internalContent || internalContent.indexOf(urlPrefix) == -1 || !doc.caretRangeFromPoint) { 24670 return; 24671 } 24672 24673 internalContent = unescape(internalContent.substr(urlPrefix.length)); 24674 if (doc.caretRangeFromPoint) { 24675 e.preventDefault(); 24676 24677 // Safari has a weird issue where drag/dropping images sometimes 24678 // produces a green plus icon. When this happens the caretRangeFromPoint 24679 // will return "null" even though the x, y coordinate is correct. 24680 // But if we detach the insert from the drop event we will get a proper range 24681 window.setTimeout(function() { 24682 var pointRng = doc.caretRangeFromPoint(e.x, e.y); 24683 24684 if (dragStartRng) { 24685 selection.setRng(dragStartRng); 24686 dragStartRng = null; 24687 } 24688 24689 customDelete(); 24690 24691 selection.setRng(pointRng); 24692 editor.insertContent(internalContent); 24693 }, 0); 24694 } 24695 24696 } 24697 }); 24698 24699 editor.on('cut', function(e) { 24700 if (!isDefaultPrevented(e) && e.clipboardData) { 24701 e.preventDefault(); 24702 e.clipboardData.clearData(); 24703 e.clipboardData.setData('text/html', editor.selection.getContent()); 24704 e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'})); 24705 customDelete(true); 24706 } 24707 }); 24708 } 24709 24710 /** 24711 * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. 24712 * 24713 * For example: 24714 * <p><b>|</b></p> 24715 * 24716 * Or: 24717 * <h1>|</h1> 24718 * 24719 * Or: 24720 * [<h1></h1>] 24721 */ 24722 function emptyEditorWhenDeleting() { 24723 function serializeRng(rng) { 24724 var body = dom.create("body"); 24725 var contents = rng.cloneContents(); 24726 body.appendChild(contents); 24727 return selection.serializer.serialize(body, {format: 'html'}); 24728 } 24729 24730 function allContentsSelected(rng) { 24731 if (!rng.setStart) { 24732 if (rng.item) { 24733 return false; 24734 } 24735 24736 var bodyRng = rng.duplicate(); 24737 bodyRng.moveToElementText(editor.getBody()); 24738 return RangeUtils.compareRanges(rng, bodyRng); 24739 } 24740 24741 var selection = serializeRng(rng); 24742 24743 var allRng = dom.createRng(); 24744 allRng.selectNode(editor.getBody()); 24745 24746 var allSelection = serializeRng(allRng); 24747 return selection === allSelection; 24748 } 24749 24750 editor.on('keydown', function(e) { 24751 var keyCode = e.keyCode, isCollapsed, body; 24752 24753 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 24754 if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { 24755 isCollapsed = editor.selection.isCollapsed(); 24756 body = editor.getBody(); 24757 24758 // Selection is collapsed but the editor isn't empty 24759 if (isCollapsed && !dom.isEmpty(body)) { 24760 return; 24761 } 24762 24763 // Selection isn't collapsed but not all the contents is selected 24764 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 24765 return; 24766 } 24767 24768 // Manually empty the editor 24769 e.preventDefault(); 24770 editor.setContent(''); 24771 24772 if (body.firstChild && dom.isBlock(body.firstChild)) { 24773 editor.selection.setCursorLocation(body.firstChild, 0); 24774 } else { 24775 editor.selection.setCursorLocation(body, 0); 24776 } 24777 24778 editor.nodeChanged(); 24779 } 24780 }); 24781 } 24782 24783 /** 24784 * WebKit doesn't select all the nodes in the body when you press Ctrl+A. 24785 * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438 24786 * This selects the whole body so that backspace/delete logic will delete everything 24787 */ 24788 function selectAll() { 24789 editor.shortcuts.add('ctrl+a', null, 'SelectAll'); 24790 } 24791 24792 /** 24793 * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. 24794 * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. 24795 * 24796 * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until 24797 * you enter a character into the editor. 24798 * 24799 * It also happens when the first focus in made to the body. 24800 * 24801 * See: https://bugs.webkit.org/show_bug.cgi?id=83566 24802 */ 24803 function inputMethodFocus() { 24804 if (!editor.settings.content_editable) { 24805 // Case 1 IME doesn't initialize if you focus the document 24806 dom.bind(editor.getDoc(), 'focusin', function() { 24807 selection.setRng(selection.getRng()); 24808 }); 24809 24810 // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event 24811 // Needs to be both down/up due to weird rendering bug on Chrome Windows 24812 dom.bind(editor.getDoc(), 'mousedown mouseup', function(e) { 24813 if (e.target == editor.getDoc().documentElement) { 24814 editor.getBody().focus(); 24815 24816 if (e.type == 'mousedown') { 24817 // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret 24818 selection.placeCaretAt(e.clientX, e.clientY); 24819 } else { 24820 selection.setRng(selection.getRng()); 24821 } 24822 } 24823 }); 24824 } 24825 } 24826 24827 /** 24828 * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the 24829 * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is 24830 * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js 24831 * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other 24832 * browsers. 24833 * 24834 * It also fixes a bug on Firefox where it's impossible to delete HR elements. 24835 */ 24836 function removeHrOnBackspace() { 24837 editor.on('keydown', function(e) { 24838 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 24839 // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow 24840 if (!editor.getBody().getElementsByTagName('hr').length) { 24841 return; 24842 } 24843 24844 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 24845 var node = selection.getNode(); 24846 var previousSibling = node.previousSibling; 24847 24848 if (node.nodeName == 'HR') { 24849 dom.remove(node); 24850 e.preventDefault(); 24851 return; 24852 } 24853 24854 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { 24855 dom.remove(previousSibling); 24856 e.preventDefault(); 24857 } 24858 } 24859 } 24860 }); 24861 } 24862 24863 /** 24864 * Firefox 3.x has an issue where the body element won't get proper focus if you click out 24865 * side it's rectangle. 24866 */ 24867 function focusBody() { 24868 // Fix for a focus bug in FF 3.x where the body element 24869 // wouldn't get proper focus if the user clicked on the HTML element 24870 if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 24871 editor.on('mousedown', function(e) { 24872 if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { 24873 var body = editor.getBody(); 24874 24875 // Blur the body it's focused but not correctly focused 24876 body.blur(); 24877 24878 // Refocus the body after a little while 24879 setTimeout(function() { 24880 body.focus(); 24881 }, 0); 24882 } 24883 }); 24884 } 24885 } 24886 24887 /** 24888 * WebKit has a bug where it isn't possible to select image, hr or anchor elements 24889 * by clicking on them so we need to fake that. 24890 */ 24891 function selectControlElements() { 24892 editor.on('click', function(e) { 24893 var target = e.target; 24894 24895 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 24896 // WebKit can't even do simple things like selecting an image 24897 // Needs to be the setBaseAndExtend or it will fail to select floated images 24898 if (/^(IMG|HR)$/.test(target.nodeName)) { 24899 e.preventDefault(); 24900 selection.getSel().setBaseAndExtent(target, 0, target, 1); 24901 editor.nodeChanged(); 24902 } 24903 24904 if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) { 24905 e.preventDefault(); 24906 selection.select(target); 24907 } 24908 }); 24909 } 24910 24911 /** 24912 * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. 24913 * 24914 * Fixes do backspace/delete on this: 24915 * <p>bla[ck</p><p style="color:red">r]ed</p> 24916 * 24917 * Would become: 24918 * <p>bla|ed</p> 24919 * 24920 * Instead of: 24921 * <p style="color:red">bla|ed</p> 24922 */ 24923 function removeStylesWhenDeletingAcrossBlockElements() { 24924 function getAttributeApplyFunction() { 24925 var template = dom.getAttribs(selection.getStart().cloneNode(false)); 24926 24927 return function() { 24928 var target = selection.getStart(); 24929 24930 if (target !== editor.getBody()) { 24931 dom.setAttrib(target, "style", null); 24932 24933 each(template, function(attr) { 24934 target.setAttributeNode(attr.cloneNode(true)); 24935 }); 24936 } 24937 }; 24938 } 24939 24940 function isSelectionAcrossElements() { 24941 return !selection.isCollapsed() && 24942 dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); 24943 } 24944 24945 editor.on('keypress', function(e) { 24946 var applyAttributes; 24947 24948 if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { 24949 applyAttributes = getAttributeApplyFunction(); 24950 editor.getDoc().execCommand('delete', false, null); 24951 applyAttributes(); 24952 e.preventDefault(); 24953 return false; 24954 } 24955 }); 24956 24957 dom.bind(editor.getDoc(), 'cut', function(e) { 24958 var applyAttributes; 24959 24960 if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { 24961 applyAttributes = getAttributeApplyFunction(); 24962 24963 setTimeout(function() { 24964 applyAttributes(); 24965 }, 0); 24966 } 24967 }); 24968 } 24969 24970 /** 24971 * Screen readers on IE needs to have the role application set on the body. 24972 */ 24973 function ensureBodyHasRoleApplication() { 24974 document.body.setAttribute("role", "application"); 24975 } 24976 24977 /** 24978 * Backspacing into a table behaves differently depending upon browser type. 24979 * Therefore, disable Backspace when cursor immediately follows a table. 24980 */ 24981 function disableBackspaceIntoATable() { 24982 editor.on('keydown', function(e) { 24983 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 24984 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 24985 var previousSibling = selection.getNode().previousSibling; 24986 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { 24987 e.preventDefault(); 24988 return false; 24989 } 24990 } 24991 } 24992 }); 24993 } 24994 24995 /** 24996 * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this 24997 * logic adds a \n before the BR so that it will get rendered. 24998 */ 24999 function addNewLinesBeforeBrInPre() { 25000 // IE8+ rendering mode does the right thing with BR in PRE 25001 if (getDocumentMode() > 7) { 25002 return; 25003 } 25004 25005 // Enable display: none in area and add a specific class that hides all BR elements in PRE to 25006 // avoid the caret from getting stuck at the BR elements while pressing the right arrow key 25007 setEditorCommandState('RespectVisibilityInDesign', true); 25008 editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); 25009 dom.addClass(editor.getBody(), 'mceHideBrInPre'); 25010 25011 // Adds a \n before all BR elements in PRE to get them visual 25012 parser.addNodeFilter('pre', function(nodes) { 25013 var i = nodes.length, brNodes, j, brElm, sibling; 25014 25015 while (i--) { 25016 brNodes = nodes[i].getAll('br'); 25017 j = brNodes.length; 25018 while (j--) { 25019 brElm = brNodes[j]; 25020 25021 // Add \n before BR in PRE elements on older IE:s so the new lines get rendered 25022 sibling = brElm.prev; 25023 if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { 25024 sibling.value += '\n'; 25025 } else { 25026 brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n'; 25027 } 25028 } 25029 } 25030 }); 25031 25032 // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible 25033 serializer.addNodeFilter('pre', function(nodes) { 25034 var i = nodes.length, brNodes, j, brElm, sibling; 25035 25036 while (i--) { 25037 brNodes = nodes[i].getAll('br'); 25038 j = brNodes.length; 25039 while (j--) { 25040 brElm = brNodes[j]; 25041 sibling = brElm.prev; 25042 if (sibling && sibling.type == 3) { 25043 sibling.value = sibling.value.replace(/\r?\n$/, ''); 25044 } 25045 } 25046 } 25047 }); 25048 } 25049 25050 /** 25051 * Moves style width/height to attribute width/height when the user resizes an image on IE. 25052 */ 25053 function removePreSerializedStylesWhenSelectingControls() { 25054 dom.bind(editor.getBody(), 'mouseup', function() { 25055 var value, node = selection.getNode(); 25056 25057 // Moved styles to attributes on IMG eements 25058 if (node.nodeName == 'IMG') { 25059 // Convert style width to width attribute 25060 if ((value = dom.getStyle(node, 'width'))) { 25061 dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); 25062 dom.setStyle(node, 'width', ''); 25063 } 25064 25065 // Convert style height to height attribute 25066 if ((value = dom.getStyle(node, 'height'))) { 25067 dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); 25068 dom.setStyle(node, 'height', ''); 25069 } 25070 } 25071 }); 25072 } 25073 25074 /** 25075 * Removes a blockquote when backspace is pressed at the beginning of it. 25076 * 25077 * For example: 25078 * <blockquote><p>|x</p></blockquote> 25079 * 25080 * Becomes: 25081 * <p>|x</p> 25082 */ 25083 function removeBlockQuoteOnBackSpace() { 25084 // Add block quote deletion handler 25085 editor.on('keydown', function(e) { 25086 var rng, container, offset, root, parent; 25087 25088 if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { 25089 return; 25090 } 25091 25092 rng = selection.getRng(); 25093 container = rng.startContainer; 25094 offset = rng.startOffset; 25095 root = dom.getRoot(); 25096 parent = container; 25097 25098 if (!rng.collapsed || offset !== 0) { 25099 return; 25100 } 25101 25102 while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { 25103 parent = parent.parentNode; 25104 } 25105 25106 // Is the cursor at the beginning of a blockquote? 25107 if (parent.tagName === 'BLOCKQUOTE') { 25108 // Remove the blockquote 25109 editor.formatter.toggle('blockquote', null, parent); 25110 25111 // Move the caret to the beginning of container 25112 rng = dom.createRng(); 25113 rng.setStart(container, 0); 25114 rng.setEnd(container, 0); 25115 selection.setRng(rng); 25116 } 25117 }); 25118 } 25119 25120 /** 25121 * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. 25122 */ 25123 function setGeckoEditingOptions() { 25124 function setOpts() { 25125 editor._refreshContentEditable(); 25126 25127 setEditorCommandState("StyleWithCSS", false); 25128 setEditorCommandState("enableInlineTableEditing", false); 25129 25130 if (!settings.object_resizing) { 25131 setEditorCommandState("enableObjectResizing", false); 25132 } 25133 } 25134 25135 if (!settings.readonly) { 25136 editor.on('BeforeExecCommand MouseDown', setOpts); 25137 } 25138 } 25139 25140 /** 25141 * Fixes a gecko link bug, when a link is placed at the end of block elements there is 25142 * no way to move the caret behind the link. This fix adds a bogus br element after the link. 25143 * 25144 * For example this: 25145 * <p><b><a href="#">x</a></b></p> 25146 * 25147 * Becomes this: 25148 * <p><b><a href="#">x</a></b><br></p> 25149 */ 25150 function addBrAfterLastLinks() { 25151 function fixLinks() { 25152 each(dom.select('a'), function(node) { 25153 var parentNode = node.parentNode, root = dom.getRoot(); 25154 25155 if (parentNode.lastChild === node) { 25156 while (parentNode && !dom.isBlock(parentNode)) { 25157 if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { 25158 return; 25159 } 25160 25161 parentNode = parentNode.parentNode; 25162 } 25163 25164 dom.add(parentNode, 'br', {'data-mce-bogus': 1}); 25165 } 25166 }); 25167 } 25168 25169 editor.on('SetContent ExecCommand', function(e) { 25170 if (e.type == "setcontent" || e.command === 'mceInsertLink') { 25171 fixLinks(); 25172 } 25173 }); 25174 } 25175 25176 /** 25177 * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by 25178 * default we want to change that behavior. 25179 */ 25180 function setDefaultBlockType() { 25181 if (settings.forced_root_block) { 25182 editor.on('init', function() { 25183 setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); 25184 }); 25185 } 25186 } 25187 25188 /** 25189 * Removes ghost selections from images/tables on Gecko. 25190 */ 25191 function removeGhostSelection() { 25192 editor.on('Undo Redo SetContent', function(e) { 25193 if (!e.initial) { 25194 editor.execCommand('mceRepaint'); 25195 } 25196 }); 25197 } 25198 25199 /** 25200 * Deletes the selected image on IE instead of navigating to previous page. 25201 */ 25202 function deleteControlItemOnBackSpace() { 25203 editor.on('keydown', function(e) { 25204 var rng; 25205 25206 if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { 25207 rng = editor.getDoc().selection.createRange(); 25208 if (rng && rng.item) { 25209 e.preventDefault(); 25210 editor.undoManager.beforeChange(); 25211 dom.remove(rng.item(0)); 25212 editor.undoManager.add(); 25213 } 25214 } 25215 }); 25216 } 25217 25218 /** 25219 * IE10 doesn't properly render block elements with the right height until you add contents to them. 25220 * This fixes that by adding a padding-right to all empty text block elements. 25221 * See: https://connect.microsoft.com/IE/feedback/details/743881 25222 */ 25223 function renderEmptyBlocksFix() { 25224 var emptyBlocksCSS; 25225 25226 // IE10+ 25227 if (getDocumentMode() >= 10) { 25228 emptyBlocksCSS = ''; 25229 each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { 25230 emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; 25231 }); 25232 25233 editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); 25234 } 25235 } 25236 25237 /** 25238 * Old IE versions can't retain contents within noscript elements so this logic will store the contents 25239 * as a attribute and the insert that value as it's raw text when the DOM is serialized. 25240 */ 25241 function keepNoScriptContents() { 25242 if (getDocumentMode() < 9) { 25243 parser.addNodeFilter('noscript', function(nodes) { 25244 var i = nodes.length, node, textNode; 25245 25246 while (i--) { 25247 node = nodes[i]; 25248 textNode = node.firstChild; 25249 25250 if (textNode) { 25251 node.attr('data-mce-innertext', textNode.value); 25252 } 25253 } 25254 }); 25255 25256 serializer.addNodeFilter('noscript', function(nodes) { 25257 var i = nodes.length, node, textNode, value; 25258 25259 while (i--) { 25260 node = nodes[i]; 25261 textNode = nodes[i].firstChild; 25262 25263 if (textNode) { 25264 textNode.value = Entities.decode(textNode.value); 25265 } else { 25266 // Old IE can't retain noscript value so an attribute is used to store it 25267 value = node.attributes.map['data-mce-innertext']; 25268 if (value) { 25269 node.attr('data-mce-innertext', null); 25270 textNode = new Node('#text', 3); 25271 textNode.value = value; 25272 textNode.raw = true; 25273 node.append(textNode); 25274 } 25275 } 25276 } 25277 }); 25278 } 25279 } 25280 25281 /** 25282 * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode. 25283 */ 25284 function fixCaretSelectionOfDocumentElementOnIe() { 25285 var doc = dom.doc, body = doc.body, started, startRng, htmlElm; 25286 25287 // Return range from point or null if it failed 25288 function rngFromPoint(x, y) { 25289 var rng = body.createTextRange(); 25290 25291 try { 25292 rng.moveToPoint(x, y); 25293 } catch (ex) { 25294 // IE sometimes throws and exception, so lets just ignore it 25295 rng = null; 25296 } 25297 25298 return rng; 25299 } 25300 25301 // Fires while the selection is changing 25302 function selectionChange(e) { 25303 var pointRng; 25304 25305 // Check if the button is down or not 25306 if (e.button) { 25307 // Create range from mouse position 25308 pointRng = rngFromPoint(e.x, e.y); 25309 25310 if (pointRng) { 25311 // Check if pointRange is before/after selection then change the endPoint 25312 if (pointRng.compareEndPoints('StartToStart', startRng) > 0) { 25313 pointRng.setEndPoint('StartToStart', startRng); 25314 } else { 25315 pointRng.setEndPoint('EndToEnd', startRng); 25316 } 25317 25318 pointRng.select(); 25319 } 25320 } else { 25321 endSelection(); 25322 } 25323 } 25324 25325 // Removes listeners 25326 function endSelection() { 25327 var rng = doc.selection.createRange(); 25328 25329 // If the range is collapsed then use the last start range 25330 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) { 25331 startRng.select(); 25332 } 25333 25334 dom.unbind(doc, 'mouseup', endSelection); 25335 dom.unbind(doc, 'mousemove', selectionChange); 25336 startRng = started = 0; 25337 } 25338 25339 // Make HTML element unselectable since we are going to handle selection by hand 25340 doc.documentElement.unselectable = true; 25341 25342 // Detect when user selects outside BODY 25343 dom.bind(doc, 'mousedown contextmenu', function(e) { 25344 if (e.target.nodeName === 'HTML') { 25345 if (started) { 25346 endSelection(); 25347 } 25348 25349 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML 25350 htmlElm = doc.documentElement; 25351 if (htmlElm.scrollHeight > htmlElm.clientHeight) { 25352 return; 25353 } 25354 25355 started = 1; 25356 // Setup start position 25357 startRng = rngFromPoint(e.x, e.y); 25358 if (startRng) { 25359 // Listen for selection change events 25360 dom.bind(doc, 'mouseup', endSelection); 25361 dom.bind(doc, 'mousemove', selectionChange); 25362 25363 dom.getRoot().focus(); 25364 startRng.select(); 25365 } 25366 } 25367 }); 25368 } 25369 25370 /** 25371 * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b> 25372 * this fix will lean the caret right into the closest inline element. 25373 */ 25374 function normalizeSelection() { 25375 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything 25376 editor.on('keyup focusin mouseup', function(e) { 25377 if (e.keyCode != 65 || !VK.metaKeyPressed(e)) { 25378 selection.normalize(); 25379 } 25380 }, true); 25381 } 25382 25383 /** 25384 * Forces Gecko to render a broken image icon if it fails to load an image. 25385 */ 25386 function showBrokenImageIcon() { 25387 editor.contentStyles.push( 25388 'img:-moz-broken {' + 25389 '-moz-force-broken-image-icon:1;' + 25390 'min-width:24px;' + 25391 'min-height:24px' + 25392 '}' 25393 ); 25394 } 25395 25396 /** 25397 * iOS has a bug where it's impossible to type if the document has a touchstart event 25398 * bound and the user touches the document while having the on screen keyboard visible. 25399 * 25400 * The touch event moves the focus to the parent document while having the caret inside the iframe 25401 * this fix moves the focus back into the iframe document. 25402 */ 25403 function restoreFocusOnKeyDown() { 25404 if (!editor.inline) { 25405 editor.on('keydown', function() { 25406 if (document.activeElement == document.body) { 25407 editor.getWin().focus(); 25408 } 25409 }); 25410 } 25411 } 25412 25413 /** 25414 * IE 11 has an annoying issue where you can't move focus into the editor 25415 * by clicking on the white area HTML element. We used to be able to to fix this with 25416 * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection 25417 * object it's not possible anymore. So we need to hack in a ungly CSS to force the 25418 * body to be at least 150px. If the user clicks the HTML element out side this 150px region 25419 * we simply move the focus into the first paragraph. Not ideal since you loose the 25420 * positioning of the caret but goot enough for most cases. 25421 */ 25422 function bodyHeight() { 25423 if (!editor.inline) { 25424 editor.contentStyles.push('body {min-height: 150px}'); 25425 editor.on('click', function(e) { 25426 if (e.target.nodeName == 'HTML') { 25427 editor.getBody().focus(); 25428 editor.selection.normalize(); 25429 editor.nodeChanged(); 25430 } 25431 }); 25432 } 25433 } 25434 25435 /** 25436 * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. 25437 * You might then loose all your work so we need to block that behavior and replace it with our own. 25438 */ 25439 function blockCmdArrowNavigation() { 25440 if (Env.mac) { 25441 editor.on('keydown', function(e) { 25442 if (VK.metaKeyPressed(e) && (e.keyCode == 37 || e.keyCode == 39)) { 25443 e.preventDefault(); 25444 editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'word'); 25445 } 25446 }); 25447 } 25448 } 25449 25450 /** 25451 * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin. 25452 */ 25453 function disableAutoUrlDetect() { 25454 setEditorCommandState("AutoUrlDetect", false); 25455 } 25456 25457 /** 25458 * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when 25459 * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync 25460 * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML 25461 * but not as the lastChild of the body. However is we add a BR element to the body then remove it 25462 * it doesn't seem to add these BR elements makes sence right?! 25463 * 25464 * Example of what happens: <body>text</body> becomes <body>text<br><br></body> 25465 */ 25466 function doubleTrailingBrElements() { 25467 if (!editor.inline) { 25468 editor.on('focus blur beforegetcontent', function() { 25469 var br = editor.dom.create('br'); 25470 editor.getBody().appendChild(br); 25471 br.parentNode.removeChild(br); 25472 }, true); 25473 } 25474 } 25475 25476 /** 25477 * iOS 7.1 introduced two new bugs: 25478 * 1) It's possible to open links within a contentEditable area by clicking on them. 25479 * 2) If you hold down the finger it will display the link/image touch callout menu. 25480 */ 25481 function tapLinksAndImages() { 25482 editor.on('click', function(e) { 25483 var elm = e.target; 25484 25485 do { 25486 if (elm.tagName === 'A') { 25487 e.preventDefault(); 25488 return; 25489 } 25490 } while ((elm = elm.parentNode)); 25491 }); 25492 25493 editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); 25494 } 25495 25496 /** 25497 * iOS Safari and possible other browsers have a bug where it won't fire 25498 * a click event when a contentEditable is focused. This function fakes click events 25499 * by using touchstart/touchend and measuring the time and distance travelled. 25500 */ 25501 function touchClickEvent() { 25502 editor.on('touchstart', function(e) { 25503 var elm, time, startTouch, changedTouches; 25504 25505 elm = e.target; 25506 time = new Date().getTime(); 25507 changedTouches = e.changedTouches; 25508 25509 if (!changedTouches || changedTouches.length > 1) { 25510 return; 25511 } 25512 25513 startTouch = changedTouches[0]; 25514 25515 editor.once('touchend', function(e) { 25516 var endTouch = e.changedTouches[0], args; 25517 25518 if (new Date().getTime() - time > 500) { 25519 return; 25520 } 25521 25522 if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) { 25523 return; 25524 } 25525 25526 if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) { 25527 return; 25528 } 25529 25530 args = { 25531 target: elm 25532 }; 25533 25534 each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) { 25535 args[key] = endTouch[key]; 25536 }); 25537 25538 args = editor.fire('click', args); 25539 25540 if (!args.isDefaultPrevented()) { 25541 // iOS WebKit can't place the caret properly once 25542 // you bind touch events so we need to do this manually 25543 // TODO: Expand to the closest word? Touble tap still works. 25544 editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY); 25545 editor.nodeChanged(); 25546 } 25547 }); 25548 }); 25549 } 25550 25551 /** 25552 * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. 25553 * For example this: <form><button></form> 25554 */ 25555 function blockFormSubmitInsideEditor() { 25556 editor.on('init', function() { 25557 editor.dom.bind(editor.getBody(), 'submit', function(e) { 25558 e.preventDefault(); 25559 }); 25560 }); 25561 } 25562 25563 /** 25564 * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class. 25565 * 25566 * Scenario: 25567 * 1) Create a table 2x2. 25568 * 2) Select and copy cells A2-B2. 25569 * 3) Paste and it will add BR element to table cell. 25570 */ 25571 function removeAppleInterchangeBrs() { 25572 parser.addNodeFilter('br', function(nodes) { 25573 var i = nodes.length; 25574 25575 while (i--) { 25576 if (nodes[i].attr('class') == 'Apple-interchange-newline') { 25577 nodes[i].remove(); 25578 } 25579 } 25580 }); 25581 } 25582 25583 // All browsers 25584 removeBlockQuoteOnBackSpace(); 25585 emptyEditorWhenDeleting(); 25586 normalizeSelection(); 25587 25588 // WebKit 25589 if (isWebKit) { 25590 cleanupStylesWhenDeleting(); 25591 inputMethodFocus(); 25592 selectControlElements(); 25593 setDefaultBlockType(); 25594 blockFormSubmitInsideEditor(); 25595 disableBackspaceIntoATable(); 25596 removeAppleInterchangeBrs(); 25597 touchClickEvent(); 25598 25599 // iOS 25600 if (Env.iOS) { 25601 restoreFocusOnKeyDown(); 25602 bodyHeight(); 25603 tapLinksAndImages(); 25604 } else { 25605 selectAll(); 25606 } 25607 } 25608 25609 // IE 25610 if (isIE && Env.ie < 11) { 25611 removeHrOnBackspace(); 25612 ensureBodyHasRoleApplication(); 25613 addNewLinesBeforeBrInPre(); 25614 removePreSerializedStylesWhenSelectingControls(); 25615 deleteControlItemOnBackSpace(); 25616 renderEmptyBlocksFix(); 25617 keepNoScriptContents(); 25618 fixCaretSelectionOfDocumentElementOnIe(); 25619 } 25620 25621 if (Env.ie >= 11) { 25622 bodyHeight(); 25623 doubleTrailingBrElements(); 25624 disableBackspaceIntoATable(); 25625 } 25626 25627 if (Env.ie) { 25628 selectAll(); 25629 disableAutoUrlDetect(); 25630 } 25631 25632 // Gecko 25633 if (isGecko) { 25634 removeHrOnBackspace(); 25635 focusBody(); 25636 removeStylesWhenDeletingAcrossBlockElements(); 25637 setGeckoEditingOptions(); 25638 addBrAfterLastLinks(); 25639 removeGhostSelection(); 25640 showBrokenImageIcon(); 25641 blockCmdArrowNavigation(); 25642 disableBackspaceIntoATable(); 25643 } 25644 }; 25645 }); 25646 25647 // Included from: js/tinymce/classes/util/Observable.js 25648 25649 /** 25650 * Observable.js 25651 * 25652 * Copyright, Moxiecode Systems AB 25653 * Released under LGPL License. 25654 * 25655 * License: http://www.tinymce.com/license 25656 * Contributing: http://www.tinymce.com/contributing 25657 */ 25658 25659 /** 25660 * This mixin will add event binding logic to classes. 25661 * 25662 * @mixin tinymce.util.Observable 25663 */ 25664 define("tinymce/util/Observable", [ 25665 "tinymce/util/EventDispatcher" 25666 ], function(EventDispatcher) { 25667 function getEventDispatcher(obj) { 25668 if (!obj._eventDispatcher) { 25669 obj._eventDispatcher = new EventDispatcher({ 25670 scope: obj, 25671 toggleEvent: function(name, state) { 25672 if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { 25673 obj.toggleNativeEvent(name, state); 25674 } 25675 } 25676 }); 25677 } 25678 25679 return obj._eventDispatcher; 25680 } 25681 25682 return { 25683 /** 25684 * Fires the specified event by name. 25685 * 25686 * @method fire 25687 * @param {String} name Name of the event to fire. 25688 * @param {Object?} args Event arguments. 25689 * @param {Boolean?} bubble True/false if the event is to be bubbled. 25690 * @return {Object} Event args instance passed in. 25691 * @example 25692 * instance.fire('event', {...}); 25693 */ 25694 fire: function(name, args, bubble) { 25695 var self = this; 25696 25697 // Prevent all events except the remove event after the instance has been removed 25698 if (self.removed && name !== "remove") { 25699 return args; 25700 } 25701 25702 args = getEventDispatcher(self).fire(name, args, bubble); 25703 25704 // Bubble event up to parents 25705 if (bubble !== false && self.parent) { 25706 var parent = self.parent(); 25707 while (parent && !args.isPropagationStopped()) { 25708 parent.fire(name, args, false); 25709 parent = parent.parent(); 25710 } 25711 } 25712 25713 return args; 25714 }, 25715 25716 /** 25717 * Binds an event listener to a specific event by name. 25718 * 25719 * @method on 25720 * @param {String} name Event name or space separated list of events to bind. 25721 * @param {callback} callback Callback to be executed when the event occurs. 25722 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 25723 * @return {Object} Current class instance. 25724 * @example 25725 * instance.on('event', function(e) { 25726 * // Callback logic 25727 * }); 25728 */ 25729 on: function(name, callback, prepend) { 25730 return getEventDispatcher(this).on(name, callback, prepend); 25731 }, 25732 25733 /** 25734 * Unbinds an event listener to a specific event by name. 25735 * 25736 * @method off 25737 * @param {String?} name Name of the event to unbind. 25738 * @param {callback?} callback Callback to unbind. 25739 * @return {Object} Current class instance. 25740 * @example 25741 * // Unbind specific callback 25742 * instance.off('event', handler); 25743 * 25744 * // Unbind all listeners by name 25745 * instance.off('event'); 25746 * 25747 * // Unbind all events 25748 * instance.off(); 25749 */ 25750 off: function(name, callback) { 25751 return getEventDispatcher(this).off(name, callback); 25752 }, 25753 25754 /** 25755 * Bind the event callback and once it fires the callback is removed. 25756 * 25757 * @method once 25758 * @param {String} name Name of the event to bind. 25759 * @param {callback} callback Callback to bind only once. 25760 * @return {Object} Current class instance. 25761 */ 25762 once: function(name, callback) { 25763 return getEventDispatcher(this).once(name, callback); 25764 }, 25765 25766 /** 25767 * Returns true/false if the object has a event of the specified name. 25768 * 25769 * @method hasEventListeners 25770 * @param {String} name Name of the event to check for. 25771 * @return {Boolean} true/false if the event exists or not. 25772 */ 25773 hasEventListeners: function(name) { 25774 return getEventDispatcher(this).has(name); 25775 } 25776 }; 25777 }); 25778 25779 // Included from: js/tinymce/classes/EditorObservable.js 25780 25781 /** 25782 * EditorObservable.js 25783 * 25784 * Copyright, Moxiecode Systems AB 25785 * Released under LGPL License. 25786 * 25787 * License: http://www.tinymce.com/license 25788 * Contributing: http://www.tinymce.com/contributing 25789 */ 25790 25791 /** 25792 * This mixin contains the event logic for the tinymce.Editor class. 25793 * 25794 * @mixin tinymce.EditorObservable 25795 * @extends tinymce.util.Observable 25796 */ 25797 define("tinymce/EditorObservable", [ 25798 "tinymce/util/Observable", 25799 "tinymce/dom/DOMUtils", 25800 "tinymce/util/Tools" 25801 ], function(Observable, DOMUtils, Tools) { 25802 var DOM = DOMUtils.DOM, customEventRootDelegates; 25803 25804 /** 25805 * Returns the event target so for the specified event. Some events fire 25806 * only on document, some fire on documentElement etc. This also handles the 25807 * custom event root setting where it returns that element instead of the body. 25808 * 25809 * @private 25810 * @param {tinymce.Editor} editor Editor instance to get event target from. 25811 * @param {String} eventName Name of the event for example "click". 25812 * @return {Element/Document} HTML Element or document target to bind on. 25813 */ 25814 function getEventTarget(editor, eventName) { 25815 if (eventName == 'selectionchange') { 25816 return editor.getDoc(); 25817 } 25818 25819 // Need to bind mousedown/mouseup etc to document not body in iframe mode 25820 // Since the user might click on the HTML element not the BODY 25821 if (!editor.inline && /^mouse|click|contextmenu|drop|dragover|dragend/.test(eventName)) { 25822 return editor.getDoc().documentElement; 25823 } 25824 25825 // Bind to event root instead of body if it's defined 25826 if (editor.settings.event_root) { 25827 if (!editor.eventRoot) { 25828 editor.eventRoot = DOM.select(editor.settings.event_root)[0]; 25829 } 25830 25831 return editor.eventRoot; 25832 } 25833 25834 return editor.getBody(); 25835 } 25836 25837 /** 25838 * Binds a event delegate for the specified name this delegate will fire 25839 * the event to the editor dispatcher. 25840 * 25841 * @private 25842 * @param {tinymce.Editor} editor Editor instance to get event target from. 25843 * @param {String} eventName Name of the event for example "click". 25844 */ 25845 function bindEventDelegate(editor, eventName) { 25846 var eventRootElm = getEventTarget(editor, eventName), delegate; 25847 25848 if (!editor.delegates) { 25849 editor.delegates = {}; 25850 } 25851 25852 if (editor.delegates[eventName]) { 25853 return; 25854 } 25855 25856 if (editor.settings.event_root) { 25857 if (!customEventRootDelegates) { 25858 customEventRootDelegates = {}; 25859 editor.editorManager.on('removeEditor', function() { 25860 var name; 25861 25862 if (!editor.editorManager.activeEditor) { 25863 if (customEventRootDelegates) { 25864 for (name in customEventRootDelegates) { 25865 editor.dom.unbind(getEventTarget(editor, name)); 25866 } 25867 25868 customEventRootDelegates = null; 25869 } 25870 } 25871 }); 25872 } 25873 25874 if (customEventRootDelegates[eventName]) { 25875 return; 25876 } 25877 25878 delegate = function(e) { 25879 var target = e.target, editors = editor.editorManager.editors, i = editors.length; 25880 25881 while (i--) { 25882 var body = editors[i].getBody(); 25883 25884 if (body === target || DOM.isChildOf(target, body)) { 25885 if (!editors[i].hidden) { 25886 editors[i].fire(eventName, e); 25887 } 25888 } 25889 } 25890 }; 25891 25892 customEventRootDelegates[eventName] = delegate; 25893 DOM.bind(eventRootElm, eventName, delegate); 25894 } else { 25895 delegate = function(e) { 25896 if (!editor.hidden) { 25897 editor.fire(eventName, e); 25898 } 25899 }; 25900 25901 DOM.bind(eventRootElm, eventName, delegate); 25902 editor.delegates[eventName] = delegate; 25903 } 25904 } 25905 25906 var EditorObservable = { 25907 /** 25908 * Bind any pending event delegates. This gets executed after the target body/document is created. 25909 * 25910 * @private 25911 */ 25912 bindPendingEventDelegates: function() { 25913 var self = this; 25914 25915 Tools.each(self._pendingNativeEvents, function(name) { 25916 bindEventDelegate(self, name); 25917 }); 25918 }, 25919 25920 /** 25921 * Toggles a native event on/off this is called by the EventDispatcher when 25922 * the first native event handler is added and when the last native event handler is removed. 25923 * 25924 * @private 25925 */ 25926 toggleNativeEvent: function(name, state) { 25927 var self = this; 25928 25929 if (self.settings.readonly) { 25930 return; 25931 } 25932 25933 // Never bind focus/blur since the FocusManager fakes those 25934 if (name == "focus" || name == "blur") { 25935 return; 25936 } 25937 25938 if (state) { 25939 if (self.initialized) { 25940 bindEventDelegate(self, name); 25941 } else { 25942 if (!self._pendingNativeEvents) { 25943 self._pendingNativeEvents = [name]; 25944 } else { 25945 self._pendingNativeEvents.push(name); 25946 } 25947 } 25948 } else if (self.initialized) { 25949 self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); 25950 delete self.delegates[name]; 25951 } 25952 }, 25953 25954 /** 25955 * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc. 25956 * 25957 * @private 25958 */ 25959 unbindAllNativeEvents: function() { 25960 var self = this, name; 25961 25962 if (self.delegates) { 25963 for (name in self.delegates) { 25964 self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); 25965 } 25966 25967 delete self.delegates; 25968 } 25969 25970 if (!self.inline) { 25971 self.getBody().onload = null; 25972 self.dom.unbind(self.getWin()); 25973 self.dom.unbind(self.getDoc()); 25974 } 25975 25976 self.dom.unbind(self.getBody()); 25977 self.dom.unbind(self.getContainer()); 25978 } 25979 }; 25980 25981 EditorObservable = Tools.extend({}, Observable, EditorObservable); 25982 25983 return EditorObservable; 25984 }); 25985 25986 // Included from: js/tinymce/classes/Shortcuts.js 25987 25988 /** 25989 * Shortcuts.js 25990 * 25991 * Copyright, Moxiecode Systems AB 25992 * Released under LGPL License. 25993 * 25994 * License: http://www.tinymce.com/license 25995 * Contributing: http://www.tinymce.com/contributing 25996 */ 25997 25998 /** 25999 * Contains all logic for handling of keyboard shortcuts. 26000 */ 26001 define("tinymce/Shortcuts", [ 26002 "tinymce/util/Tools", 26003 "tinymce/Env" 26004 ], function(Tools, Env) { 26005 var each = Tools.each, explode = Tools.explode; 26006 26007 var keyCodeLookup = { 26008 "f9": 120, 26009 "f10": 121, 26010 "f11": 122 26011 }; 26012 26013 return function(editor) { 26014 var self = this, shortcuts = {}; 26015 26016 editor.on('keyup keypress keydown', function(e) { 26017 if ((e.altKey || e.ctrlKey || e.metaKey) && !e.isDefaultPrevented()) { 26018 each(shortcuts, function(shortcut) { 26019 var ctrlKey = Env.mac ? e.metaKey : e.ctrlKey; 26020 26021 if (shortcut.ctrl != ctrlKey || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { 26022 return; 26023 } 26024 26025 if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { 26026 e.preventDefault(); 26027 26028 if (e.type == "keydown") { 26029 shortcut.func.call(shortcut.scope); 26030 } 26031 26032 return true; 26033 } 26034 }); 26035 } 26036 }); 26037 26038 /** 26039 * Adds a keyboard shortcut for some command or function. 26040 * 26041 * @method addShortcut 26042 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 26043 * @param {String} desc Text description for the command. 26044 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 26045 * @param {Object} sc Optional scope to execute the function in. 26046 * @return {Boolean} true/false state if the shortcut was added or not. 26047 */ 26048 self.add = function(pattern, desc, cmdFunc, scope) { 26049 var cmd; 26050 26051 cmd = cmdFunc; 26052 26053 if (typeof(cmdFunc) === 'string') { 26054 cmdFunc = function() { 26055 editor.execCommand(cmd, false, null); 26056 }; 26057 } else if (Tools.isArray(cmd)) { 26058 cmdFunc = function() { 26059 editor.execCommand(cmd[0], cmd[1], cmd[2]); 26060 }; 26061 } 26062 26063 each(explode(pattern.toLowerCase()), function(pattern) { 26064 var shortcut = { 26065 func: cmdFunc, 26066 scope: scope || editor, 26067 desc: editor.translate(desc), 26068 alt: false, 26069 ctrl: false, 26070 shift: false 26071 }; 26072 26073 each(explode(pattern, '+'), function(value) { 26074 switch (value) { 26075 case 'alt': 26076 case 'ctrl': 26077 case 'shift': 26078 shortcut[value] = true; 26079 break; 26080 26081 default: 26082 // Allow numeric keycodes like ctrl+219 for ctrl+[ 26083 if (/^[0-9]{2,}$/.test(value)) { 26084 shortcut.keyCode = parseInt(value, 10); 26085 } else { 26086 shortcut.charCode = value.charCodeAt(0); 26087 shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); 26088 } 26089 } 26090 }); 26091 26092 shortcuts[ 26093 (shortcut.ctrl ? 'ctrl' : '') + ',' + 26094 (shortcut.alt ? 'alt' : '') + ',' + 26095 (shortcut.shift ? 'shift' : '') + ',' + 26096 shortcut.keyCode 26097 ] = shortcut; 26098 }); 26099 26100 return true; 26101 }; 26102 }; 26103 }); 26104 26105 // Included from: js/tinymce/classes/Editor.js 26106 26107 /** 26108 * Editor.js 26109 * 26110 * Copyright, Moxiecode Systems AB 26111 * Released under LGPL License. 26112 * 26113 * License: http://www.tinymce.com/license 26114 * Contributing: http://www.tinymce.com/contributing 26115 */ 26116 26117 /*jshint scripturl:true */ 26118 26119 /** 26120 * Include the base event class documentation. 26121 * 26122 * @include ../../../tools/docs/tinymce.Event.js 26123 */ 26124 26125 /** 26126 * This class contains the core logic for a TinyMCE editor. 26127 * 26128 * @class tinymce.Editor 26129 * @mixes tinymce.util.Observable 26130 * @example 26131 * // Add a class to all paragraphs in the editor. 26132 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 26133 * 26134 * // Gets the current editors selection as text 26135 * tinymce.activeEditor.selection.getContent({format: 'text'}); 26136 * 26137 * // Creates a new editor instance 26138 * var ed = new tinymce.Editor('textareaid', { 26139 * some_setting: 1 26140 * }, tinymce.EditorManager); 26141 * 26142 * // Select each item the user clicks on 26143 * ed.on('click', function(e) { 26144 * ed.selection.select(e.target); 26145 * }); 26146 * 26147 * ed.render(); 26148 */ 26149 define("tinymce/Editor", [ 26150 "tinymce/dom/DOMUtils", 26151 "tinymce/dom/DomQuery", 26152 "tinymce/AddOnManager", 26153 "tinymce/NodeChange", 26154 "tinymce/html/Node", 26155 "tinymce/dom/Serializer", 26156 "tinymce/html/Serializer", 26157 "tinymce/dom/Selection", 26158 "tinymce/Formatter", 26159 "tinymce/UndoManager", 26160 "tinymce/EnterKey", 26161 "tinymce/ForceBlocks", 26162 "tinymce/EditorCommands", 26163 "tinymce/util/URI", 26164 "tinymce/dom/ScriptLoader", 26165 "tinymce/dom/EventUtils", 26166 "tinymce/WindowManager", 26167 "tinymce/html/Schema", 26168 "tinymce/html/DomParser", 26169 "tinymce/util/Quirks", 26170 "tinymce/Env", 26171 "tinymce/util/Tools", 26172 "tinymce/EditorObservable", 26173 "tinymce/Shortcuts" 26174 ], function( 26175 DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer, 26176 Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands, 26177 URI, ScriptLoader, EventUtils, WindowManager, 26178 Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts 26179 ) { 26180 // Shorten these names 26181 var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager; 26182 var extend = Tools.extend, each = Tools.each, explode = Tools.explode; 26183 var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve; 26184 var Event = EventUtils.Event; 26185 var isGecko = Env.gecko, ie = Env.ie; 26186 26187 /** 26188 * Include documentation for all the events. 26189 * 26190 * @include ../../../tools/docs/tinymce.Editor.js 26191 */ 26192 26193 /** 26194 * Constructs a editor instance by id. 26195 * 26196 * @constructor 26197 * @method Editor 26198 * @param {String} id Unique id for the editor. 26199 * @param {Object} settings Settings for the editor. 26200 * @param {tinymce.EditorManager} editorManager EditorManager instance. 26201 * @author Moxiecode 26202 */ 26203 function Editor(id, settings, editorManager) { 26204 var self = this, documentBaseUrl, baseUri; 26205 26206 documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; 26207 baseUri = editorManager.baseURI; 26208 26209 /** 26210 * Name/value collection with editor settings. 26211 * 26212 * @property settings 26213 * @type Object 26214 * @example 26215 * // Get the value of the theme setting 26216 * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); 26217 */ 26218 self.settings = settings = extend({ 26219 id: id, 26220 theme: 'modern', 26221 delta_width: 0, 26222 delta_height: 0, 26223 popup_css: '', 26224 plugins: '', 26225 document_base_url: documentBaseUrl, 26226 add_form_submit_trigger: true, 26227 submit_patch: true, 26228 add_unload_trigger: true, 26229 convert_urls: true, 26230 relative_urls: true, 26231 remove_script_host: true, 26232 object_resizing: true, 26233 doctype: '<!DOCTYPE html>', 26234 visual: true, 26235 font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large', 26236 26237 // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 26238 font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%', 26239 forced_root_block: 'p', 26240 hidden_input: true, 26241 padd_empty_editor: true, 26242 render_ui: true, 26243 indentation: '30px', 26244 inline_styles: true, 26245 convert_fonts_to_spans: true, 26246 indent: 'simple', 26247 indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,ol,li,dl,dt,dd,area,table,thead,' + 26248 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 26249 indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,ol,li,dl,dt,dd,area,table,thead,' + 26250 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 26251 validate: true, 26252 entity_encoding: 'named', 26253 url_converter: self.convertURL, 26254 url_converter_scope: self, 26255 ie7_compat: true 26256 }, settings); 26257 26258 AddOnManager.language = settings.language || 'en'; 26259 AddOnManager.languageLoad = settings.language_load; 26260 26261 AddOnManager.baseURL = editorManager.baseURL; 26262 26263 /** 26264 * Editor instance id, normally the same as the div/textarea that was replaced. 26265 * 26266 * @property id 26267 * @type String 26268 */ 26269 self.id = settings.id = id; 26270 26271 /** 26272 * State to force the editor to return false on a isDirty call. 26273 * 26274 * @property isNotDirty 26275 * @type Boolean 26276 * @example 26277 * function ajaxSave() { 26278 * var ed = tinymce.get('elm1'); 26279 * 26280 * // Save contents using some XHR call 26281 * alert(ed.getContent()); 26282 * 26283 * ed.isNotDirty = true; // Force not dirty state 26284 * } 26285 */ 26286 self.isNotDirty = true; 26287 26288 /** 26289 * Name/Value object containting plugin instances. 26290 * 26291 * @property plugins 26292 * @type Object 26293 * @example 26294 * // Execute a method inside a plugin directly 26295 * tinymce.activeEditor.plugins.someplugin.someMethod(); 26296 */ 26297 self.plugins = {}; 26298 26299 /** 26300 * URI object to document configured for the TinyMCE instance. 26301 * 26302 * @property documentBaseURI 26303 * @type tinymce.util.URI 26304 * @example 26305 * // Get relative URL from the location of document_base_url 26306 * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); 26307 * 26308 * // Get absolute URL from the location of document_base_url 26309 * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); 26310 */ 26311 self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, { 26312 base_uri: baseUri 26313 }); 26314 26315 /** 26316 * URI object to current document that holds the TinyMCE editor instance. 26317 * 26318 * @property baseURI 26319 * @type tinymce.util.URI 26320 * @example 26321 * // Get relative URL from the location of the API 26322 * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); 26323 * 26324 * // Get absolute URL from the location of the API 26325 * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); 26326 */ 26327 self.baseURI = baseUri; 26328 26329 /** 26330 * Array with CSS files to load into the iframe. 26331 * 26332 * @property contentCSS 26333 * @type Array 26334 */ 26335 self.contentCSS = []; 26336 26337 /** 26338 * Array of CSS styles to add to head of document when the editor loads. 26339 * 26340 * @property contentStyles 26341 * @type Array 26342 */ 26343 self.contentStyles = []; 26344 26345 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic 26346 self.shortcuts = new Shortcuts(self); 26347 26348 // Internal command handler objects 26349 self.execCommands = {}; 26350 self.queryStateCommands = {}; 26351 self.queryValueCommands = {}; 26352 self.loadedCSS = {}; 26353 26354 if (settings.target) { 26355 self.targetElm = settings.target; 26356 } 26357 26358 self.suffix = editorManager.suffix; 26359 self.editorManager = editorManager; 26360 self.inline = settings.inline; 26361 26362 // Call setup 26363 editorManager.fire('SetupEditor', self); 26364 self.execCallback('setup', self); 26365 26366 /** 26367 * Dom query instance with default scope to the editor document and default element is the body of the editor. 26368 * 26369 * @property $ 26370 * @type tinymce.dom.DomQuery 26371 * @example 26372 * tinymce.activeEditor.$('p').css('color', 'red'); 26373 * tinymce.activeEditor.$().append('<p>new</p>'); 26374 */ 26375 self.$ = DomQuery.overrideDefaults(function() { 26376 return { 26377 context: self.inline ? self.getBody() : self.getDoc(), 26378 element: self.getBody() 26379 }; 26380 }); 26381 } 26382 26383 Editor.prototype = { 26384 /** 26385 * Renderes the editor/adds it to the page. 26386 * 26387 * @method render 26388 */ 26389 render: function() { 26390 var self = this, settings = self.settings, id = self.id, suffix = self.suffix; 26391 26392 function readyHandler() { 26393 DOM.unbind(window, 'ready', readyHandler); 26394 self.render(); 26395 } 26396 26397 // Page is not loaded yet, wait for it 26398 if (!Event.domLoaded) { 26399 DOM.bind(window, 'ready', readyHandler); 26400 return; 26401 } 26402 26403 // Element not found, then skip initialization 26404 if (!self.getElement()) { 26405 return; 26406 } 26407 26408 // No editable support old iOS versions etc 26409 if (!Env.contentEditable) { 26410 return; 26411 } 26412 26413 // Hide target element early to prevent content flashing 26414 if (!settings.inline) { 26415 self.orgVisibility = self.getElement().style.visibility; 26416 self.getElement().style.visibility = 'hidden'; 26417 } else { 26418 self.inline = true; 26419 } 26420 26421 var form = self.getElement().form || DOM.getParent(id, 'form'); 26422 if (form) { 26423 self.formElement = form; 26424 26425 // Add hidden input for non input elements inside form elements 26426 if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) { 26427 DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id); 26428 self.hasHiddenInput = true; 26429 } 26430 26431 // Pass submit/reset from form to editor instance 26432 self.formEventDelegate = function(e) { 26433 self.fire(e.type, e); 26434 }; 26435 26436 DOM.bind(form, 'submit reset', self.formEventDelegate); 26437 26438 // Reset contents in editor when the form is reset 26439 self.on('reset', function() { 26440 self.setContent(self.startContent, {format: 'raw'}); 26441 }); 26442 26443 // Check page uses id="submit" or name="submit" for it's submit button 26444 if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { 26445 form._mceOldSubmit = form.submit; 26446 form.submit = function() { 26447 self.editorManager.triggerSave(); 26448 self.isNotDirty = true; 26449 26450 return form._mceOldSubmit(form); 26451 }; 26452 } 26453 } 26454 26455 /** 26456 * Window manager reference, use this to open new windows and dialogs. 26457 * 26458 * @property windowManager 26459 * @type tinymce.WindowManager 26460 * @example 26461 * // Shows an alert message 26462 * tinymce.activeEditor.windowManager.alert('Hello world!'); 26463 * 26464 * // Opens a new dialog with the file.htm file and the size 320x240 26465 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. 26466 * tinymce.activeEditor.windowManager.open({ 26467 * url: 'file.htm', 26468 * width: 320, 26469 * height: 240 26470 * }, { 26471 * custom_param: 1 26472 * }); 26473 */ 26474 self.windowManager = new WindowManager(self); 26475 26476 if (settings.encoding == 'xml') { 26477 self.on('GetContent', function(e) { 26478 if (e.save) { 26479 e.content = DOM.encode(e.content); 26480 } 26481 }); 26482 } 26483 26484 if (settings.add_form_submit_trigger) { 26485 self.on('submit', function() { 26486 if (self.initialized) { 26487 self.save(); 26488 } 26489 }); 26490 } 26491 26492 if (settings.add_unload_trigger) { 26493 self._beforeUnload = function() { 26494 if (self.initialized && !self.destroyed && !self.isHidden()) { 26495 self.save({format: 'raw', no_events: true, set_dirty: false}); 26496 } 26497 }; 26498 26499 self.editorManager.on('BeforeUnload', self._beforeUnload); 26500 } 26501 26502 // Load scripts 26503 function loadScripts() { 26504 var scriptLoader = ScriptLoader.ScriptLoader; 26505 26506 if (settings.language && settings.language != 'en' && !settings.language_url) { 26507 settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js'; 26508 } 26509 26510 if (settings.language_url) { 26511 scriptLoader.add(settings.language_url); 26512 } 26513 26514 if (settings.theme && typeof settings.theme != "function" && 26515 settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) { 26516 var themeUrl = settings.theme_url; 26517 26518 if (themeUrl) { 26519 themeUrl = self.documentBaseURI.toAbsolute(themeUrl); 26520 } else { 26521 themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js'; 26522 } 26523 26524 ThemeManager.load(settings.theme, themeUrl); 26525 } 26526 26527 if (Tools.isArray(settings.plugins)) { 26528 settings.plugins = settings.plugins.join(' '); 26529 } 26530 26531 each(settings.external_plugins, function(url, name) { 26532 PluginManager.load(name, url); 26533 settings.plugins += ' ' + name; 26534 }); 26535 26536 each(settings.plugins.split(/[ ,]/), function(plugin) { 26537 plugin = trim(plugin); 26538 26539 if (plugin && !PluginManager.urls[plugin]) { 26540 if (plugin.charAt(0) == '-') { 26541 plugin = plugin.substr(1, plugin.length); 26542 26543 var dependencies = PluginManager.dependencies(plugin); 26544 26545 each(dependencies, function(dep) { 26546 var defaultSettings = { 26547 prefix: 'plugins/', 26548 resource: dep, 26549 suffix: '/plugin' + suffix + '.js' 26550 }; 26551 26552 dep = PluginManager.createUrl(defaultSettings, dep); 26553 PluginManager.load(dep.resource, dep); 26554 }); 26555 } else { 26556 PluginManager.load(plugin, { 26557 prefix: 'plugins/', 26558 resource: plugin, 26559 suffix: '/plugin' + suffix + '.js' 26560 }); 26561 } 26562 } 26563 }); 26564 26565 scriptLoader.loadQueue(function() { 26566 if (!self.removed) { 26567 self.init(); 26568 } 26569 }); 26570 } 26571 26572 loadScripts(); 26573 }, 26574 26575 /** 26576 * Initializes the editor this will be called automatically when 26577 * all plugins/themes and language packs are loaded by the rendered method. 26578 * This method will setup the iframe and create the theme and plugin instances. 26579 * 26580 * @method init 26581 */ 26582 init: function() { 26583 var self = this, settings = self.settings, elm = self.getElement(); 26584 var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = []; 26585 26586 self.rtl = this.editorManager.i18n.rtl; 26587 self.editorManager.add(self); 26588 26589 settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area')); 26590 26591 /** 26592 * Reference to the theme instance that was used to generate the UI. 26593 * 26594 * @property theme 26595 * @type tinymce.Theme 26596 * @example 26597 * // Executes a method on the theme directly 26598 * tinymce.activeEditor.theme.someMethod(); 26599 */ 26600 if (settings.theme) { 26601 if (typeof settings.theme != "function") { 26602 settings.theme = settings.theme.replace(/-/, ''); 26603 Theme = ThemeManager.get(settings.theme); 26604 self.theme = new Theme(self, ThemeManager.urls[settings.theme]); 26605 26606 if (self.theme.init) { 26607 self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$); 26608 } 26609 } else { 26610 self.theme = settings.theme; 26611 } 26612 } 26613 26614 function initPlugin(plugin) { 26615 var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance; 26616 26617 pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, ''); 26618 plugin = trim(plugin); 26619 if (Plugin && inArray(initializedPlugins, plugin) === -1) { 26620 each(PluginManager.dependencies(plugin), function(dep) { 26621 initPlugin(dep); 26622 }); 26623 26624 pluginInstance = new Plugin(self, pluginUrl, self.$); 26625 26626 self.plugins[plugin] = pluginInstance; 26627 26628 if (pluginInstance.init) { 26629 pluginInstance.init(self, pluginUrl); 26630 initializedPlugins.push(plugin); 26631 } 26632 } 26633 } 26634 26635 // Create all plugins 26636 each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin); 26637 26638 // Measure box 26639 if (settings.render_ui && self.theme) { 26640 self.orgDisplay = elm.style.display; 26641 26642 if (typeof settings.theme != "function") { 26643 w = settings.width || elm.style.width || elm.offsetWidth; 26644 h = settings.height || elm.style.height || elm.offsetHeight; 26645 minHeight = settings.min_height || 100; 26646 re = /^[0-9\.]+(|px)$/i; 26647 26648 if (re.test('' + w)) { 26649 w = Math.max(parseInt(w, 10), 100); 26650 } 26651 26652 if (re.test('' + h)) { 26653 h = Math.max(parseInt(h, 10), minHeight); 26654 } 26655 26656 // Render UI 26657 o = self.theme.renderUI({ 26658 targetNode: elm, 26659 width: w, 26660 height: h, 26661 deltaWidth: settings.delta_width, 26662 deltaHeight: settings.delta_height 26663 }); 26664 26665 // Resize editor 26666 if (!settings.content_editable) { 26667 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); 26668 if (h < minHeight) { 26669 h = minHeight; 26670 } 26671 } 26672 } else { 26673 o = settings.theme(self, elm); 26674 26675 // Convert element type to id:s 26676 if (o.editorContainer.nodeType) { 26677 o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent"; 26678 } 26679 26680 // Convert element type to id:s 26681 if (o.iframeContainer.nodeType) { 26682 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer"; 26683 } 26684 26685 // Use specified iframe height or the targets offsetHeight 26686 h = o.iframeHeight || elm.offsetHeight; 26687 } 26688 26689 self.editorContainer = o.editorContainer; 26690 } 26691 26692 // Load specified content CSS last 26693 if (settings.content_css) { 26694 each(explode(settings.content_css), function(u) { 26695 self.contentCSS.push(self.documentBaseURI.toAbsolute(u)); 26696 }); 26697 } 26698 26699 // Load specified content CSS last 26700 if (settings.content_style) { 26701 self.contentStyles.push(settings.content_style); 26702 } 26703 26704 // Content editable mode ends here 26705 if (settings.content_editable) { 26706 elm = n = o = null; // Fix IE leak 26707 return self.initContentBody(); 26708 } 26709 26710 self.iframeHTML = settings.doctype + '<html><head>'; 26711 26712 // We only need to override paths if we have to 26713 // IE has a bug where it remove site absolute urls to relative ones if this is specified 26714 if (settings.document_base_url != self.documentBaseUrl) { 26715 self.iframeHTML += '<base href="' + self.documentBaseURI.getURI() + '" />'; 26716 } 26717 26718 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. 26719 if (!Env.caretAfter && settings.ie7_compat) { 26720 self.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; 26721 } 26722 26723 self.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 26724 26725 // Load the CSS by injecting them into the HTML this will reduce "flicker" 26726 for (i = 0; i < self.contentCSS.length; i++) { 26727 var cssUrl = self.contentCSS[i]; 26728 self.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + cssUrl + '" />'; 26729 self.loadedCSS[cssUrl] = true; 26730 } 26731 26732 bodyId = settings.body_id || 'tinymce'; 26733 if (bodyId.indexOf('=') != -1) { 26734 bodyId = self.getParam('body_id', '', 'hash'); 26735 bodyId = bodyId[self.id] || bodyId; 26736 } 26737 26738 bodyClass = settings.body_class || ''; 26739 if (bodyClass.indexOf('=') != -1) { 26740 bodyClass = self.getParam('body_class', '', 'hash'); 26741 bodyClass = bodyClass[self.id] || ''; 26742 } 26743 26744 if (settings.content_security_policy) { 26745 self.iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + settings.content_security_policy + '" />'; 26746 } 26747 26748 self.iframeHTML += '</head><body id="' + bodyId + 26749 '" class="mce-content-body ' + bodyClass + 26750 '" data-id="' + self.id + '"><br></body></html>'; 26751 26752 /*eslint no-script-url:0 */ 26753 var domainRelaxUrl = 'javascript:(function(){' + 26754 'document.open();document.domain="' + document.domain + '";' + 26755 'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' + 26756 'document.close();ed.initContentBody(true);})()'; 26757 26758 // Domain relaxing is required since the user has messed around with document.domain 26759 if (document.domain != location.hostname) { 26760 url = domainRelaxUrl; 26761 } 26762 26763 // Create iframe 26764 // TODO: ACC add the appropriate description on this. 26765 var ifr = DOM.create('iframe', { 26766 id: self.id + "_ifr", 26767 //src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7 26768 frameBorder: '0', 26769 allowTransparency: "true", 26770 title: self.editorManager.translate( 26771 "Rich Text Area. Press ALT-F9 for menu. " + 26772 "Press ALT-F10 for toolbar. Press ALT-0 for help" 26773 ), 26774 style: { 26775 width: '100%', 26776 height: h, 26777 display: 'block' // Important for Gecko to render the iframe correctly 26778 } 26779 }); 26780 26781 ifr.onload = function() { 26782 ifr.onload = null; 26783 self.fire("load"); 26784 }; 26785 26786 DOM.setAttrib(ifr, "src", url || 'javascript:""'); 26787 26788 self.contentAreaContainer = o.iframeContainer; 26789 self.iframeElement = ifr; 26790 26791 n = DOM.add(o.iframeContainer, ifr); 26792 26793 // Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname 26794 // Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!! 26795 if (ie) { 26796 try { 26797 self.getDoc(); 26798 } catch (e) { 26799 n.src = url = domainRelaxUrl; 26800 } 26801 } 26802 26803 if (o.editorContainer) { 26804 DOM.get(o.editorContainer).style.display = self.orgDisplay; 26805 self.hidden = DOM.isHidden(o.editorContainer); 26806 } 26807 26808 self.getElement().style.display = 'none'; 26809 DOM.setAttrib(self.id, 'aria-hidden', true); 26810 26811 if (!url) { 26812 self.initContentBody(); 26813 } 26814 26815 elm = n = o = null; // Cleanup 26816 }, 26817 26818 /** 26819 * This method get called by the init method ones the iframe is loaded. 26820 * It will fill the iframe with contents, setups DOM and selection objects for the iframe. 26821 * 26822 * @method initContentBody 26823 * @private 26824 */ 26825 initContentBody: function(skipWrite) { 26826 var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText; 26827 26828 // Restore visibility on target element 26829 if (!settings.inline) { 26830 self.getElement().style.visibility = self.orgVisibility; 26831 } 26832 26833 // Setup iframe body 26834 if (!skipWrite && !settings.content_editable) { 26835 doc.open(); 26836 doc.write(self.iframeHTML); 26837 doc.close(); 26838 } 26839 26840 if (settings.content_editable) { 26841 self.on('remove', function() { 26842 var bodyEl = this.getBody(); 26843 26844 DOM.removeClass(bodyEl, 'mce-content-body'); 26845 DOM.removeClass(bodyEl, 'mce-edit-focus'); 26846 DOM.setAttrib(bodyEl, 'contentEditable', null); 26847 }); 26848 26849 DOM.addClass(targetElm, 'mce-content-body'); 26850 self.contentDocument = doc = settings.content_document || document; 26851 self.contentWindow = settings.content_window || window; 26852 self.bodyElement = targetElm; 26853 26854 // Prevent leak in IE 26855 settings.content_document = settings.content_window = null; 26856 26857 // TODO: Fix this 26858 settings.root_name = targetElm.nodeName.toLowerCase(); 26859 } 26860 26861 // It will not steal focus while setting contentEditable 26862 body = self.getBody(); 26863 body.disabled = true; 26864 26865 if (!settings.readonly) { 26866 if (self.inline && DOM.getStyle(body, 'position', true) == 'static') { 26867 body.style.position = 'relative'; 26868 } 26869 26870 body.contentEditable = self.getParam('content_editable_state', true); 26871 } 26872 26873 body.disabled = false; 26874 26875 /** 26876 * Schema instance, enables you to validate elements and it's children. 26877 * 26878 * @property schema 26879 * @type tinymce.html.Schema 26880 */ 26881 self.schema = new Schema(settings); 26882 26883 /** 26884 * DOM instance for the editor. 26885 * 26886 * @property dom 26887 * @type tinymce.dom.DOMUtils 26888 * @example 26889 * // Adds a class to all paragraphs within the editor 26890 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 26891 */ 26892 self.dom = new DOMUtils(doc, { 26893 keep_values: true, 26894 url_converter: self.convertURL, 26895 url_converter_scope: self, 26896 hex_colors: settings.force_hex_style_colors, 26897 class_filter: settings.class_filter, 26898 update_styles: true, 26899 root_element: self.inline ? self.getBody() : null, 26900 collect: settings.content_editable, 26901 schema: self.schema, 26902 onSetAttrib: function(e) { 26903 self.fire('SetAttrib', e); 26904 } 26905 }); 26906 26907 /** 26908 * HTML parser will be used when contents is inserted into the editor. 26909 * 26910 * @property parser 26911 * @type tinymce.html.DomParser 26912 */ 26913 self.parser = new DomParser(settings, self.schema); 26914 26915 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 26916 self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) { 26917 var i = nodes.length, node, dom = self.dom, value, internalName; 26918 26919 while (i--) { 26920 node = nodes[i]; 26921 value = node.attr(name); 26922 internalName = 'data-mce-' + name; 26923 26924 // Add internal attribute if we need to we don't on a refresh of the document 26925 if (!node.attributes.map[internalName]) { 26926 if (name === "style") { 26927 value = dom.serializeStyle(dom.parseStyle(value), node.name); 26928 26929 if (!value.length) { 26930 value = null; 26931 } 26932 26933 node.attr(internalName, value); 26934 node.attr(name, value); 26935 } else if (name === "tabindex") { 26936 node.attr(internalName, value); 26937 node.attr(name, null); 26938 } else { 26939 node.attr(internalName, self.convertURL(value, name, node.name)); 26940 } 26941 } 26942 } 26943 }); 26944 26945 // Keep scripts from executing 26946 self.parser.addNodeFilter('script', function(nodes) { 26947 var i = nodes.length, node; 26948 26949 while (i--) { 26950 node = nodes[i]; 26951 node.attr('type', 'mce-' + (node.attr('type') || 'no/type')); 26952 } 26953 }); 26954 26955 self.parser.addNodeFilter('#cdata', function(nodes) { 26956 var i = nodes.length, node; 26957 26958 while (i--) { 26959 node = nodes[i]; 26960 node.type = 8; 26961 node.name = '#comment'; 26962 node.value = '[CDATA[' + node.value + ']]'; 26963 } 26964 }); 26965 26966 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) { 26967 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); 26968 26969 while (i--) { 26970 node = nodes[i]; 26971 26972 if (node.isEmpty(nonEmptyElements)) { 26973 node.append(new Node('br', 1)).shortEnded = true; 26974 } 26975 } 26976 }); 26977 26978 /** 26979 * DOM serializer for the editor. Will be used when contents is extracted from the editor. 26980 * 26981 * @property serializer 26982 * @type tinymce.dom.Serializer 26983 * @example 26984 * // Serializes the first paragraph in the editor into a string 26985 * tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]); 26986 */ 26987 self.serializer = new DomSerializer(settings, self); 26988 26989 /** 26990 * Selection instance for the editor. 26991 * 26992 * @property selection 26993 * @type tinymce.dom.Selection 26994 * @example 26995 * // Sets some contents to the current selection in the editor 26996 * tinymce.activeEditor.selection.setContent('Some contents'); 26997 * 26998 * // Gets the current selection 26999 * alert(tinymce.activeEditor.selection.getContent()); 27000 * 27001 * // Selects the first paragraph found 27002 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 27003 */ 27004 self.selection = new Selection(self.dom, self.getWin(), self.serializer, self); 27005 27006 /** 27007 * Formatter instance. 27008 * 27009 * @property formatter 27010 * @type tinymce.Formatter 27011 */ 27012 self.formatter = new Formatter(self); 27013 27014 /** 27015 * Undo manager instance, responsible for handling undo levels. 27016 * 27017 * @property undoManager 27018 * @type tinymce.UndoManager 27019 * @example 27020 * // Undoes the last modification to the editor 27021 * tinymce.activeEditor.undoManager.undo(); 27022 */ 27023 self.undoManager = new UndoManager(self); 27024 27025 self.forceBlocks = new ForceBlocks(self); 27026 self.enterKey = new EnterKey(self); 27027 self.editorCommands = new EditorCommands(self); 27028 self._nodeChangeDispatcher = new NodeChange(self); 27029 27030 self.fire('PreInit'); 27031 27032 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { 27033 doc.body.spellcheck = false; // Gecko 27034 DOM.setAttrib(body, "spellcheck", "false"); 27035 } 27036 27037 self.fire('PostRender'); 27038 27039 self.quirks = new Quirks(self); 27040 27041 if (settings.directionality) { 27042 body.dir = settings.directionality; 27043 } 27044 27045 if (settings.nowrap) { 27046 body.style.whiteSpace = "nowrap"; 27047 } 27048 27049 if (settings.protect) { 27050 self.on('BeforeSetContent', function(e) { 27051 each(settings.protect, function(pattern) { 27052 e.content = e.content.replace(pattern, function(str) { 27053 return '<!--mce:protected ' + escape(str) + '-->'; 27054 }); 27055 }); 27056 }); 27057 } 27058 27059 self.on('SetContent', function() { 27060 self.addVisual(self.getBody()); 27061 }); 27062 27063 // Remove empty contents 27064 if (settings.padd_empty_editor) { 27065 self.on('PostProcess', function(e) { 27066 e.content = e.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); 27067 }); 27068 } 27069 27070 self.load({initial: true, format: 'html'}); 27071 self.startContent = self.getContent({format: 'raw'}); 27072 27073 /** 27074 * Is set to true after the editor instance has been initialized 27075 * 27076 * @property initialized 27077 * @type Boolean 27078 * @example 27079 * function isEditorInitialized(editor) { 27080 * return editor && editor.initialized; 27081 * } 27082 */ 27083 self.initialized = true; 27084 self.bindPendingEventDelegates(); 27085 27086 self.fire('init'); 27087 self.focus(true); 27088 self.nodeChanged({initial: true}); 27089 self.execCallback('init_instance_callback', self); 27090 27091 // Add editor specific CSS styles 27092 if (self.contentStyles.length > 0) { 27093 contentCssText = ''; 27094 27095 each(self.contentStyles, function(style) { 27096 contentCssText += style + "\r\n"; 27097 }); 27098 27099 self.dom.addStyle(contentCssText); 27100 } 27101 27102 // Load specified content CSS last 27103 each(self.contentCSS, function(cssUrl) { 27104 if (!self.loadedCSS[cssUrl]) { 27105 self.dom.loadCSS(cssUrl); 27106 self.loadedCSS[cssUrl] = true; 27107 } 27108 }); 27109 27110 // Handle auto focus 27111 if (settings.auto_focus) { 27112 setTimeout(function() { 27113 var editor; 27114 27115 if (settings.auto_focus === true) { 27116 editor = self; 27117 } else { 27118 editor = self.editorManager.get(settings.auto_focus); 27119 } 27120 27121 editor.focus(); 27122 }, 100); 27123 } 27124 27125 // Clean up references for IE 27126 targetElm = doc = body = null; 27127 }, 27128 27129 /** 27130 * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection 27131 * it will also place DOM focus inside the editor. 27132 * 27133 * @method focus 27134 * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor. 27135 */ 27136 focus: function(skipFocus) { 27137 var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng; 27138 var controlElm, doc = self.getDoc(), body; 27139 27140 if (!skipFocus) { 27141 // Get selected control element 27142 rng = selection.getRng(); 27143 if (rng.item) { 27144 controlElm = rng.item(0); 27145 } 27146 27147 self._refreshContentEditable(); 27148 27149 // Focus the window iframe 27150 if (!contentEditable) { 27151 // WebKit needs this call to fire focusin event properly see #5948 27152 // But Opera pre Blink engine will produce an empty selection so skip Opera 27153 if (!Env.opera) { 27154 self.getBody().focus(); 27155 } 27156 27157 self.getWin().focus(); 27158 } 27159 27160 // Focus the body as well since it's contentEditable 27161 if (isGecko || contentEditable) { 27162 body = self.getBody(); 27163 27164 // Check for setActive since it doesn't scroll to the element 27165 if (body.setActive) { 27166 // IE 11 sometimes throws "Invalid function" then fallback to focus 27167 try { 27168 body.setActive(); 27169 } catch (ex) { 27170 body.focus(); 27171 } 27172 } else { 27173 body.focus(); 27174 } 27175 27176 if (contentEditable) { 27177 selection.normalize(); 27178 } 27179 } 27180 27181 // Restore selected control element 27182 // This is needed when for example an image is selected within a 27183 // layer a call to focus will then remove the control selection 27184 if (controlElm && controlElm.ownerDocument == doc) { 27185 rng = doc.body.createControlRange(); 27186 rng.addElement(controlElm); 27187 rng.select(); 27188 } 27189 } 27190 27191 self.editorManager.setActive(self); 27192 }, 27193 27194 /** 27195 * Executes a legacy callback. This method is useful to call old 2.x option callbacks. 27196 * There new event model is a better way to add callback so this method might be removed in the future. 27197 * 27198 * @method execCallback 27199 * @param {String} name Name of the callback to execute. 27200 * @return {Object} Return value passed from callback function. 27201 */ 27202 execCallback: function(name) { 27203 var self = this, callback = self.settings[name], scope; 27204 27205 if (!callback) { 27206 return; 27207 } 27208 27209 // Look through lookup 27210 if (self.callbackLookup && (scope = self.callbackLookup[name])) { 27211 callback = scope.func; 27212 scope = scope.scope; 27213 } 27214 27215 if (typeof(callback) === 'string') { 27216 scope = callback.replace(/\.\w+$/, ''); 27217 scope = scope ? resolve(scope) : 0; 27218 callback = resolve(callback); 27219 self.callbackLookup = self.callbackLookup || {}; 27220 self.callbackLookup[name] = {func: callback, scope: scope}; 27221 } 27222 27223 return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1)); 27224 }, 27225 27226 /** 27227 * Translates the specified string by replacing variables with language pack items it will also check if there is 27228 * a key mathcin the input. 27229 * 27230 * @method translate 27231 * @param {String} text String to translate by the language pack data. 27232 * @return {String} Translated string. 27233 */ 27234 translate: function(text) { 27235 var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; 27236 27237 if (!text) { 27238 return ''; 27239 } 27240 27241 return i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) { 27242 return i18n.data[lang + '.' + b] || '{#' + b + '}'; 27243 }); 27244 }, 27245 27246 /** 27247 * Returns a language pack item by name/key. 27248 * 27249 * @method getLang 27250 * @param {String} name Name/key to get from the language pack. 27251 * @param {String} defaultVal Optional default value to retrive. 27252 */ 27253 getLang: function(name, defaultVal) { 27254 return ( 27255 this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] || 27256 (defaultVal !== undefined ? defaultVal : '{#' + name + '}') 27257 ); 27258 }, 27259 27260 /** 27261 * Returns a configuration parameter by name. 27262 * 27263 * @method getParam 27264 * @param {String} name Configruation parameter to retrive. 27265 * @param {String} defaultVal Optional default value to return. 27266 * @param {String} type Optional type parameter. 27267 * @return {String} Configuration parameter value or default value. 27268 * @example 27269 * // Returns a specific config value from the currently active editor 27270 * var someval = tinymce.activeEditor.getParam('myvalue'); 27271 * 27272 * // Returns a specific config value from a specific editor instance by id 27273 * var someval2 = tinymce.get('my_editor').getParam('myvalue'); 27274 */ 27275 getParam: function(name, defaultVal, type) { 27276 var value = name in this.settings ? this.settings[name] : defaultVal, output; 27277 27278 if (type === 'hash') { 27279 output = {}; 27280 27281 if (typeof(value) === 'string') { 27282 each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) { 27283 value = value.split('='); 27284 27285 if (value.length > 1) { 27286 output[trim(value[0])] = trim(value[1]); 27287 } else { 27288 output[trim(value[0])] = trim(value); 27289 } 27290 }); 27291 } else { 27292 output = value; 27293 } 27294 27295 return output; 27296 } 27297 27298 return value; 27299 }, 27300 27301 /** 27302 * Distpaches out a onNodeChange event to all observers. This method should be called when you 27303 * need to update the UI states or element path etc. 27304 * 27305 * @method nodeChanged 27306 * @param {Object} args Optional args to pass to NodeChange event handlers. 27307 */ 27308 nodeChanged: function(args) { 27309 this._nodeChangeDispatcher.nodeChanged(args); 27310 }, 27311 27312 /** 27313 * Adds a button that later gets created by the theme in the editors toolbars. 27314 * 27315 * @method addButton 27316 * @param {String} name Button name to add. 27317 * @param {Object} settings Settings object with title, cmd etc. 27318 * @example 27319 * // Adds a custom button to the editor that inserts contents when clicked 27320 * tinymce.init({ 27321 * ... 27322 * 27323 * toolbar: 'example' 27324 * 27325 * setup: function(ed) { 27326 * ed.addButton('example', { 27327 * title: 'My title', 27328 * image: '../js/tinymce/plugins/example/img/example.gif', 27329 * onclick: function() { 27330 * ed.insertContent('Hello world!!'); 27331 * } 27332 * }); 27333 * } 27334 * }); 27335 */ 27336 addButton: function(name, settings) { 27337 var self = this; 27338 27339 if (settings.cmd) { 27340 settings.onclick = function() { 27341 self.execCommand(settings.cmd); 27342 }; 27343 } 27344 27345 if (!settings.text && !settings.icon) { 27346 settings.icon = name; 27347 } 27348 27349 self.buttons = self.buttons || {}; 27350 settings.tooltip = settings.tooltip || settings.title; 27351 self.buttons[name] = settings; 27352 }, 27353 27354 /** 27355 * Adds a menu item to be used in the menus of the theme. There might be multiple instances 27356 * of this menu item for example it might be used in the main menus of the theme but also in 27357 * the context menu so make sure that it's self contained and supports multiple instances. 27358 * 27359 * @method addMenuItem 27360 * @param {String} name Menu item name to add. 27361 * @param {Object} settings Settings object with title, cmd etc. 27362 * @example 27363 * // Adds a custom menu item to the editor that inserts contents when clicked 27364 * // The context option allows you to add the menu item to an existing default menu 27365 * tinymce.init({ 27366 * ... 27367 * 27368 * setup: function(ed) { 27369 * ed.addMenuItem('example', { 27370 * text: 'My menu item', 27371 * context: 'tools', 27372 * onclick: function() { 27373 * ed.insertContent('Hello world!!'); 27374 * } 27375 * }); 27376 * } 27377 * }); 27378 */ 27379 addMenuItem: function(name, settings) { 27380 var self = this; 27381 27382 if (settings.cmd) { 27383 settings.onclick = function() { 27384 self.execCommand(settings.cmd); 27385 }; 27386 } 27387 27388 self.menuItems = self.menuItems || {}; 27389 self.menuItems[name] = settings; 27390 }, 27391 27392 /** 27393 * Adds a custom command to the editor, you can also override existing commands with this method. 27394 * The command that you add can be executed with execCommand. 27395 * 27396 * @method addCommand 27397 * @param {String} name Command name to add/override. 27398 * @param {addCommandCallback} callback Function to execute when the command occurs. 27399 * @param {Object} scope Optional scope to execute the function in. 27400 * @example 27401 * // Adds a custom command that later can be executed using execCommand 27402 * tinymce.init({ 27403 * ... 27404 * 27405 * setup: function(ed) { 27406 * // Register example command 27407 * ed.addCommand('mycommand', function(ui, v) { 27408 * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'})); 27409 * }); 27410 * } 27411 * }); 27412 */ 27413 addCommand: function(name, callback, scope) { 27414 /** 27415 * Callback function that gets called when a command is executed. 27416 * 27417 * @callback addCommandCallback 27418 * @param {Boolean} ui Display UI state true/false. 27419 * @param {Object} value Optional value for command. 27420 * @return {Boolean} True/false state if the command was handled or not. 27421 */ 27422 this.execCommands[name] = {func: callback, scope: scope || this}; 27423 }, 27424 27425 /** 27426 * Adds a custom query state command to the editor, you can also override existing commands with this method. 27427 * The command that you add can be executed with queryCommandState function. 27428 * 27429 * @method addQueryStateHandler 27430 * @param {String} name Command name to add/override. 27431 * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrival occurs. 27432 * @param {Object} scope Optional scope to execute the function in. 27433 */ 27434 addQueryStateHandler: function(name, callback, scope) { 27435 /** 27436 * Callback function that gets called when a queryCommandState is executed. 27437 * 27438 * @callback addQueryStateHandlerCallback 27439 * @return {Boolean} True/false state if the command is enabled or not like is it bold. 27440 */ 27441 this.queryStateCommands[name] = {func: callback, scope: scope || this}; 27442 }, 27443 27444 /** 27445 * Adds a custom query value command to the editor, you can also override existing commands with this method. 27446 * The command that you add can be executed with queryCommandValue function. 27447 * 27448 * @method addQueryValueHandler 27449 * @param {String} name Command name to add/override. 27450 * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrival occurs. 27451 * @param {Object} scope Optional scope to execute the function in. 27452 */ 27453 addQueryValueHandler: function(name, callback, scope) { 27454 /** 27455 * Callback function that gets called when a queryCommandValue is executed. 27456 * 27457 * @callback addQueryValueHandlerCallback 27458 * @return {Object} Value of the command or undefined. 27459 */ 27460 this.queryValueCommands[name] = {func: callback, scope: scope || this}; 27461 }, 27462 27463 /** 27464 * Adds a keyboard shortcut for some command or function. 27465 * 27466 * @method addShortcut 27467 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 27468 * @param {String} desc Text description for the command. 27469 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 27470 * @param {Object} sc Optional scope to execute the function in. 27471 * @return {Boolean} true/false state if the shortcut was added or not. 27472 */ 27473 addShortcut: function(pattern, desc, cmdFunc, scope) { 27474 this.shortcuts.add(pattern, desc, cmdFunc, scope); 27475 }, 27476 27477 /** 27478 * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or 27479 * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org. 27480 * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these 27481 * return true it will handle the command as a internal browser command. 27482 * 27483 * @method execCommand 27484 * @param {String} cmd Command name to execute, for example mceLink or Bold. 27485 * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not. 27486 * @param {mixed} value Optional command value, this can be anything. 27487 * @param {Object} a Optional arguments object. 27488 */ 27489 execCommand: function(cmd, ui, value, args) { 27490 var self = this, state = 0, cmdItem; 27491 27492 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(cmd) && (!args || !args.skip_focus)) { 27493 self.focus(); 27494 } 27495 27496 args = extend({}, args); 27497 args = self.fire('BeforeExecCommand', {command: cmd, ui: ui, value: value}); 27498 if (args.isDefaultPrevented()) { 27499 return false; 27500 } 27501 27502 // Registred commands 27503 if ((cmdItem = self.execCommands[cmd])) { 27504 // Fall through on true 27505 if (cmdItem.func.call(cmdItem.scope, ui, value) !== true) { 27506 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27507 return true; 27508 } 27509 } 27510 27511 // Plugin commands 27512 each(self.plugins, function(p) { 27513 if (p.execCommand && p.execCommand(cmd, ui, value)) { 27514 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27515 state = true; 27516 return false; 27517 } 27518 }); 27519 27520 if (state) { 27521 return state; 27522 } 27523 27524 // Theme commands 27525 if (self.theme && self.theme.execCommand && self.theme.execCommand(cmd, ui, value)) { 27526 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27527 return true; 27528 } 27529 27530 // Editor commands 27531 if (self.editorCommands.execCommand(cmd, ui, value)) { 27532 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27533 return true; 27534 } 27535 27536 // Browser commands 27537 try { 27538 state = self.getDoc().execCommand(cmd, ui, value); 27539 } catch (ex) { 27540 // Ignore old IE errors 27541 } 27542 27543 if (state) { 27544 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27545 return true; 27546 } 27547 27548 return false; 27549 }, 27550 27551 /** 27552 * Returns a command specific state, for example if bold is enabled or not. 27553 * 27554 * @method queryCommandState 27555 * @param {string} cmd Command to query state from. 27556 * @return {Boolean} Command specific state, for example if bold is enabled or not. 27557 */ 27558 queryCommandState: function(cmd) { 27559 var self = this, queryItem, returnVal; 27560 27561 // Is hidden then return undefined 27562 if (self._isHidden()) { 27563 return; 27564 } 27565 27566 // Registred commands 27567 if ((queryItem = self.queryStateCommands[cmd])) { 27568 returnVal = queryItem.func.call(queryItem.scope); 27569 27570 // Fall though on non boolean returns 27571 if (returnVal === true || returnVal === false) { 27572 return returnVal; 27573 } 27574 } 27575 27576 // Editor commands 27577 returnVal = self.editorCommands.queryCommandState(cmd); 27578 if (returnVal !== -1) { 27579 return returnVal; 27580 } 27581 27582 // Browser commands 27583 try { 27584 return self.getDoc().queryCommandState(cmd); 27585 } catch (ex) { 27586 // Fails sometimes see bug: 1896577 27587 } 27588 }, 27589 27590 /** 27591 * Returns a command specific value, for example the current font size. 27592 * 27593 * @method queryCommandValue 27594 * @param {string} cmd Command to query value from. 27595 * @return {Object} Command specific value, for example the current font size. 27596 */ 27597 queryCommandValue: function(cmd) { 27598 var self = this, queryItem, returnVal; 27599 27600 // Is hidden then return undefined 27601 if (self._isHidden()) { 27602 return; 27603 } 27604 27605 // Registred commands 27606 if ((queryItem = self.queryValueCommands[cmd])) { 27607 returnVal = queryItem.func.call(queryItem.scope); 27608 27609 // Fall though on true 27610 if (returnVal !== true) { 27611 return returnVal; 27612 } 27613 } 27614 27615 // Editor commands 27616 returnVal = self.editorCommands.queryCommandValue(cmd); 27617 if (returnVal !== undefined) { 27618 return returnVal; 27619 } 27620 27621 // Browser commands 27622 try { 27623 return self.getDoc().queryCommandValue(cmd); 27624 } catch (ex) { 27625 // Fails sometimes see bug: 1896577 27626 } 27627 }, 27628 27629 /** 27630 * Shows the editor and hides any textarea/div that the editor is supposed to replace. 27631 * 27632 * @method show 27633 */ 27634 show: function() { 27635 var self = this; 27636 27637 if (self.hidden) { 27638 self.hidden = false; 27639 27640 if (self.inline) { 27641 self.getBody().contentEditable = true; 27642 } else { 27643 DOM.show(self.getContainer()); 27644 DOM.hide(self.id); 27645 } 27646 27647 self.load(); 27648 self.fire('show'); 27649 } 27650 }, 27651 27652 /** 27653 * Hides the editor and shows any textarea/div that the editor is supposed to replace. 27654 * 27655 * @method hide 27656 */ 27657 hide: function() { 27658 var self = this, doc = self.getDoc(); 27659 27660 if (!self.hidden) { 27661 // Fixed bug where IE has a blinking cursor left from the editor 27662 if (ie && doc && !self.inline) { 27663 doc.execCommand('SelectAll'); 27664 } 27665 27666 // We must save before we hide so Safari doesn't crash 27667 self.save(); 27668 27669 if (self.inline) { 27670 self.getBody().contentEditable = false; 27671 27672 // Make sure the editor gets blurred 27673 if (self == self.editorManager.focusedEditor) { 27674 self.editorManager.focusedEditor = null; 27675 } 27676 } else { 27677 DOM.hide(self.getContainer()); 27678 DOM.setStyle(self.id, 'display', self.orgDisplay); 27679 } 27680 27681 self.hidden = true; 27682 self.fire('hide'); 27683 } 27684 }, 27685 27686 /** 27687 * Returns true/false if the editor is hidden or not. 27688 * 27689 * @method isHidden 27690 * @return {Boolean} True/false if the editor is hidden or not. 27691 */ 27692 isHidden: function() { 27693 return !!this.hidden; 27694 }, 27695 27696 /** 27697 * Sets the progress state, this will display a throbber/progess for the editor. 27698 * This is ideal for asycronous operations like an AJAX save call. 27699 * 27700 * @method setProgressState 27701 * @param {Boolean} state Boolean state if the progress should be shown or hidden. 27702 * @param {Number} time Optional time to wait before the progress gets shown. 27703 * @return {Boolean} Same as the input state. 27704 * @example 27705 * // Show progress for the active editor 27706 * tinymce.activeEditor.setProgressState(true); 27707 * 27708 * // Hide progress for the active editor 27709 * tinymce.activeEditor.setProgressState(false); 27710 * 27711 * // Show progress after 3 seconds 27712 * tinymce.activeEditor.setProgressState(true, 3000); 27713 */ 27714 setProgressState: function(state, time) { 27715 this.fire('ProgressState', {state: state, time: time}); 27716 }, 27717 27718 /** 27719 * Loads contents from the textarea or div element that got converted into an editor instance. 27720 * This method will move the contents from that textarea or div into the editor by using setContent 27721 * so all events etc that method has will get dispatched as well. 27722 * 27723 * @method load 27724 * @param {Object} args Optional content object, this gets passed around through the whole load process. 27725 * @return {String} HTML string that got set into the editor. 27726 */ 27727 load: function(args) { 27728 var self = this, elm = self.getElement(), html; 27729 27730 if (elm) { 27731 args = args || {}; 27732 args.load = true; 27733 27734 html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args); 27735 args.element = elm; 27736 27737 if (!args.no_events) { 27738 self.fire('LoadContent', args); 27739 } 27740 27741 args.element = elm = null; 27742 27743 return html; 27744 } 27745 }, 27746 27747 /** 27748 * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance. 27749 * This method will move the HTML contents from the editor into that textarea or div by getContent 27750 * so all events etc that method has will get dispatched as well. 27751 * 27752 * @method save 27753 * @param {Object} args Optional content object, this gets passed around through the whole save process. 27754 * @return {String} HTML string that got set into the textarea/div. 27755 */ 27756 save: function(args) { 27757 var self = this, elm = self.getElement(), html, form; 27758 27759 if (!elm || !self.initialized) { 27760 return; 27761 } 27762 27763 args = args || {}; 27764 args.save = true; 27765 27766 args.element = elm; 27767 html = args.content = self.getContent(args); 27768 27769 if (!args.no_events) { 27770 self.fire('SaveContent', args); 27771 } 27772 27773 html = args.content; 27774 27775 if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) { 27776 // Update DIV element when not in inline mode 27777 if (!self.inline) { 27778 elm.innerHTML = html; 27779 } 27780 27781 // Update hidden form element 27782 if ((form = DOM.getParent(self.id, 'form'))) { 27783 each(form.elements, function(elm) { 27784 if (elm.name == self.id) { 27785 elm.value = html; 27786 return false; 27787 } 27788 }); 27789 } 27790 } else { 27791 elm.value = html; 27792 } 27793 27794 args.element = elm = null; 27795 27796 if (args.set_dirty !== false) { 27797 self.isNotDirty = true; 27798 } 27799 27800 return html; 27801 }, 27802 27803 /** 27804 * Sets the specified content to the editor instance, this will cleanup the content before it gets set using 27805 * the different cleanup rules options. 27806 * 27807 * @method setContent 27808 * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well. 27809 * @param {Object} args Optional content object, this gets passed around through the whole set process. 27810 * @return {String} HTML string that got set into the editor. 27811 * @example 27812 * // Sets the HTML contents of the activeEditor editor 27813 * tinymce.activeEditor.setContent('<span>some</span> html'); 27814 * 27815 * // Sets the raw contents of the activeEditor editor 27816 * tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'}); 27817 * 27818 * // Sets the content of a specific editor (my_editor in this example) 27819 * tinymce.get('my_editor').setContent(data); 27820 * 27821 * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added 27822 * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'}); 27823 */ 27824 setContent: function(content, args) { 27825 var self = this, body = self.getBody(), forcedRootBlockName; 27826 27827 // Setup args object 27828 args = args || {}; 27829 args.format = args.format || 'html'; 27830 args.set = true; 27831 args.content = content; 27832 27833 // Do preprocessing 27834 if (!args.no_events) { 27835 self.fire('BeforeSetContent', args); 27836 } 27837 27838 content = args.content; 27839 27840 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 27841 // It will also be impossible to place the caret in the editor unless there is a BR element present 27842 if (content.length === 0 || /^\s+$/.test(content)) { 27843 forcedRootBlockName = self.settings.forced_root_block; 27844 27845 // Check if forcedRootBlock is configured and that the block is a valid child of the body 27846 if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { 27847 // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly 27848 content = ie && ie < 11 ? '' : '<br data-mce-bogus="1">'; 27849 content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content); 27850 } else if (!ie) { 27851 // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret 27852 content = '<br data-mce-bogus="1">'; 27853 } 27854 27855 self.dom.setHTML(body, content); 27856 27857 self.fire('SetContent', args); 27858 } else { 27859 // Parse and serialize the html 27860 if (args.format !== 'raw') { 27861 content = new Serializer({}, self.schema).serialize( 27862 self.parser.parse(content, {isRootContent: true}) 27863 ); 27864 } 27865 27866 // Set the new cleaned contents to the editor 27867 args.content = trim(content); 27868 self.dom.setHTML(body, args.content); 27869 27870 // Do post processing 27871 if (!args.no_events) { 27872 self.fire('SetContent', args); 27873 } 27874 27875 // Don't normalize selection if the focused element isn't the body in 27876 // content editable mode since it will steal focus otherwise 27877 /*if (!self.settings.content_editable || document.activeElement === self.getBody()) { 27878 self.selection.normalize(); 27879 }*/ 27880 } 27881 27882 return args.content; 27883 }, 27884 27885 /** 27886 * Gets the content from the editor instance, this will cleanup the content before it gets returned using 27887 * the different cleanup rules options. 27888 * 27889 * @method getContent 27890 * @param {Object} args Optional content object, this gets passed around through the whole get process. 27891 * @return {String} Cleaned content string, normally HTML contents. 27892 * @example 27893 * // Get the HTML contents of the currently active editor 27894 * console.debug(tinymce.activeEditor.getContent()); 27895 * 27896 * // Get the raw contents of the currently active editor 27897 * tinymce.activeEditor.getContent({format: 'raw'}); 27898 * 27899 * // Get content of a specific editor: 27900 * tinymce.get('content id').getContent() 27901 */ 27902 getContent: function(args) { 27903 var self = this, content, body = self.getBody(); 27904 27905 // Setup args object 27906 args = args || {}; 27907 args.format = args.format || 'html'; 27908 args.get = true; 27909 args.getInner = true; 27910 27911 // Do preprocessing 27912 if (!args.no_events) { 27913 self.fire('BeforeGetContent', args); 27914 } 27915 27916 // Get raw contents or by default the cleaned contents 27917 if (args.format == 'raw') { 27918 content = body.innerHTML; 27919 } else if (args.format == 'text') { 27920 content = body.innerText || body.textContent; 27921 } else { 27922 content = self.serializer.serialize(body, args); 27923 } 27924 27925 // Trim whitespace in beginning/end of HTML 27926 if (args.format != 'text') { 27927 args.content = trim(content); 27928 } else { 27929 args.content = content; 27930 } 27931 27932 // Do post processing 27933 if (!args.no_events) { 27934 self.fire('GetContent', args); 27935 } 27936 27937 return args.content; 27938 }, 27939 27940 /** 27941 * Inserts content at caret position. 27942 * 27943 * @method insertContent 27944 * @param {String} content Content to insert. 27945 * @param {Object} args Optional args to pass to insert call. 27946 */ 27947 insertContent: function(content, args) { 27948 if (args) { 27949 content = extend({content: content}, args); 27950 } 27951 27952 this.execCommand('mceInsertContent', false, content); 27953 }, 27954 27955 /** 27956 * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 27957 * 27958 * @method isDirty 27959 * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 27960 * @example 27961 * if (tinymce.activeEditor.isDirty()) 27962 * alert("You must save your contents."); 27963 */ 27964 isDirty: function() { 27965 return !this.isNotDirty; 27966 }, 27967 27968 /** 27969 * Returns the editors container element. The container element wrappes in 27970 * all the elements added to the page for the editor. Such as UI, iframe etc. 27971 * 27972 * @method getContainer 27973 * @return {Element} HTML DOM element for the editor container. 27974 */ 27975 getContainer: function() { 27976 var self = this; 27977 27978 if (!self.container) { 27979 self.container = DOM.get(self.editorContainer || self.id + '_parent'); 27980 } 27981 27982 return self.container; 27983 }, 27984 27985 /** 27986 * Returns the editors content area container element. The this element is the one who 27987 * holds the iframe or the editable element. 27988 * 27989 * @method getContentAreaContainer 27990 * @return {Element} HTML DOM element for the editor area container. 27991 */ 27992 getContentAreaContainer: function() { 27993 return this.contentAreaContainer; 27994 }, 27995 27996 /** 27997 * Returns the target element/textarea that got replaced with a TinyMCE editor instance. 27998 * 27999 * @method getElement 28000 * @return {Element} HTML DOM element for the replaced element. 28001 */ 28002 getElement: function() { 28003 if (!this.targetElm) { 28004 this.targetElm = DOM.get(this.id); 28005 } 28006 28007 return this.targetElm; 28008 }, 28009 28010 /** 28011 * Returns the iframes window object. 28012 * 28013 * @method getWin 28014 * @return {Window} Iframe DOM window object. 28015 */ 28016 getWin: function() { 28017 var self = this, elm; 28018 28019 if (!self.contentWindow) { 28020 elm = self.iframeElement; 28021 28022 if (elm) { 28023 self.contentWindow = elm.contentWindow; 28024 } 28025 } 28026 28027 return self.contentWindow; 28028 }, 28029 28030 /** 28031 * Returns the iframes document object. 28032 * 28033 * @method getDoc 28034 * @return {Document} Iframe DOM document object. 28035 */ 28036 getDoc: function() { 28037 var self = this, win; 28038 28039 if (!self.contentDocument) { 28040 win = self.getWin(); 28041 28042 if (win) { 28043 self.contentDocument = win.document; 28044 } 28045 } 28046 28047 return self.contentDocument; 28048 }, 28049 28050 /** 28051 * Returns the iframes body element. 28052 * 28053 * @method getBody 28054 * @return {Element} Iframe body element. 28055 */ 28056 getBody: function() { 28057 return this.bodyElement || this.getDoc().body; 28058 }, 28059 28060 /** 28061 * URL converter function this gets executed each time a user adds an img, a or 28062 * any other element that has a URL in it. This will be called both by the DOM and HTML 28063 * manipulation functions. 28064 * 28065 * @method convertURL 28066 * @param {string} url URL to convert. 28067 * @param {string} name Attribute name src, href etc. 28068 * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert. 28069 * @return {string} Converted URL string. 28070 */ 28071 convertURL: function(url, name, elm) { 28072 var self = this, settings = self.settings; 28073 28074 // Use callback instead 28075 if (settings.urlconverter_callback) { 28076 return self.execCallback('urlconverter_callback', url, elm, true, name); 28077 } 28078 28079 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs 28080 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) { 28081 return url; 28082 } 28083 28084 // Convert to relative 28085 if (settings.relative_urls) { 28086 return self.documentBaseURI.toRelative(url); 28087 } 28088 28089 // Convert to absolute 28090 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); 28091 28092 return url; 28093 }, 28094 28095 /** 28096 * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor. 28097 * 28098 * @method addVisual 28099 * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid. 28100 */ 28101 addVisual: function(elm) { 28102 var self = this, settings = self.settings, dom = self.dom, cls; 28103 28104 elm = elm || self.getBody(); 28105 28106 if (self.hasVisual === undefined) { 28107 self.hasVisual = settings.visual; 28108 } 28109 28110 each(dom.select('table,a', elm), function(elm) { 28111 var value; 28112 28113 switch (elm.nodeName) { 28114 case 'TABLE': 28115 cls = settings.visual_table_class || 'mce-item-table'; 28116 value = dom.getAttrib(elm, 'border'); 28117 28118 if ((!value || value == '0') && self.hasVisual) { 28119 dom.addClass(elm, cls); 28120 } else { 28121 dom.removeClass(elm, cls); 28122 } 28123 28124 return; 28125 28126 case 'A': 28127 if (!dom.getAttrib(elm, 'href', false)) { 28128 value = dom.getAttrib(elm, 'name') || elm.id; 28129 cls = settings.visual_anchor_class || 'mce-item-anchor'; 28130 28131 if (value && self.hasVisual) { 28132 dom.addClass(elm, cls); 28133 } else { 28134 dom.removeClass(elm, cls); 28135 } 28136 } 28137 28138 return; 28139 } 28140 }); 28141 28142 self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual}); 28143 }, 28144 28145 /** 28146 * Removes the editor from the dom and tinymce collection. 28147 * 28148 * @method remove 28149 */ 28150 remove: function() { 28151 var self = this; 28152 28153 if (!self.removed) { 28154 self.save(); 28155 self.removed = 1; 28156 self.unbindAllNativeEvents(); 28157 28158 // Remove any hidden input 28159 if (self.hasHiddenInput) { 28160 DOM.remove(self.getElement().nextSibling); 28161 } 28162 28163 if (!self.inline) { 28164 // IE 9 has a bug where the selection stops working if you place the 28165 // caret inside the editor then remove the iframe 28166 if (ie && ie < 10) { 28167 self.getDoc().execCommand('SelectAll', false, null); 28168 } 28169 28170 DOM.setStyle(self.id, 'display', self.orgDisplay); 28171 self.getBody().onload = null; // Prevent #6816 28172 } 28173 28174 self.fire('remove'); 28175 28176 self.editorManager.remove(self); 28177 DOM.remove(self.getContainer()); 28178 self.destroy(); 28179 } 28180 }, 28181 28182 /** 28183 * Destroys the editor instance by removing all events, element references or other resources 28184 * that could leak memory. This method will be called automatically when the page is unloaded 28185 * but you can also call it directly if you know what you are doing. 28186 * 28187 * @method destroy 28188 * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one. 28189 */ 28190 destroy: function(automatic) { 28191 var self = this, form; 28192 28193 // One time is enough 28194 if (self.destroyed) { 28195 return; 28196 } 28197 28198 // If user manually calls destroy and not remove 28199 // Users seems to have logic that calls destroy instead of remove 28200 if (!automatic && !self.removed) { 28201 self.remove(); 28202 return; 28203 } 28204 28205 if (!automatic) { 28206 self.editorManager.off('beforeunload', self._beforeUnload); 28207 28208 // Manual destroy 28209 if (self.theme && self.theme.destroy) { 28210 self.theme.destroy(); 28211 } 28212 28213 // Destroy controls, selection and dom 28214 self.selection.destroy(); 28215 self.dom.destroy(); 28216 } 28217 28218 form = self.formElement; 28219 if (form) { 28220 if (form._mceOldSubmit) { 28221 form.submit = form._mceOldSubmit; 28222 form._mceOldSubmit = null; 28223 } 28224 28225 DOM.unbind(form, 'submit reset', self.formEventDelegate); 28226 } 28227 28228 self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null; 28229 self.bodyElement = self.contentDocument = self.contentWindow = null; 28230 self.iframeElement = self.targetElm = null; 28231 28232 if (self.selection) { 28233 self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null; 28234 } 28235 28236 self.destroyed = 1; 28237 }, 28238 28239 // Internal functions 28240 28241 _refreshContentEditable: function() { 28242 var self = this, body, parent; 28243 28244 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again 28245 if (self._isHidden()) { 28246 body = self.getBody(); 28247 parent = body.parentNode; 28248 28249 parent.removeChild(body); 28250 parent.appendChild(body); 28251 28252 body.focus(); 28253 } 28254 }, 28255 28256 _isHidden: function() { 28257 var sel; 28258 28259 if (!isGecko) { 28260 return 0; 28261 } 28262 28263 // Weird, wheres that cursor selection? 28264 sel = this.selection.getSel(); 28265 return (!sel || !sel.rangeCount || sel.rangeCount === 0); 28266 } 28267 }; 28268 28269 extend(Editor.prototype, EditorObservable); 28270 28271 return Editor; 28272 }); 28273 28274 // Included from: js/tinymce/classes/util/I18n.js 28275 28276 /** 28277 * I18n.js 28278 * 28279 * Copyright, Moxiecode Systems AB 28280 * Released under LGPL License. 28281 * 28282 * License: http://www.tinymce.com/license 28283 * Contributing: http://www.tinymce.com/contributing 28284 */ 28285 28286 /** 28287 * I18n class that handles translation of TinyMCE UI. 28288 * Uses po style with csharp style parameters. 28289 * 28290 * @class tinymce.util.I18n 28291 */ 28292 define("tinymce/util/I18n", [], function() { 28293 "use strict"; 28294 28295 var data = {}; 28296 28297 return { 28298 /** 28299 * Property gets set to true if a RTL language pack was loaded. 28300 * 28301 * @property rtl 28302 * @type Boolean 28303 */ 28304 rtl: false, 28305 28306 /** 28307 * Adds translations for a specific language code. 28308 * 28309 * @method add 28310 * @param {String} code Language code like sv_SE. 28311 * @param {Array} items Name/value array with English en_US to sv_SE. 28312 */ 28313 add: function(code, items) { 28314 for (var name in items) { 28315 data[name] = items[name]; 28316 } 28317 28318 this.rtl = this.rtl || data._dir === 'rtl'; 28319 }, 28320 28321 /** 28322 * Translates the specified text. 28323 * 28324 * It has a few formats: 28325 * I18n.translate("Text"); 28326 * I18n.translate(["Text {0}/{1}", 0, 1]); 28327 * I18n.translate({raw: "Raw string"}); 28328 * 28329 * @method translate 28330 * @param {String/Object/Array} text Text to translate. 28331 * @return {String} String that got translated. 28332 */ 28333 translate: function(text) { 28334 if (typeof(text) == "undefined") { 28335 return text; 28336 } 28337 28338 if (typeof(text) != "string" && text.raw) { 28339 return text.raw; 28340 } 28341 28342 if (text.push) { 28343 var values = text.slice(1); 28344 28345 text = (data[text[0]] || text[0]).replace(/\{([^\}]+)\}/g, function(match1, match2) { 28346 return values[match2]; 28347 }); 28348 } 28349 28350 return data[text] || text; 28351 }, 28352 28353 data: data 28354 }; 28355 }); 28356 28357 // Included from: js/tinymce/classes/FocusManager.js 28358 28359 /** 28360 * FocusManager.js 28361 * 28362 * Copyright, Moxiecode Systems AB 28363 * Released under LGPL License. 28364 * 28365 * License: http://www.tinymce.com/license 28366 * Contributing: http://www.tinymce.com/contributing 28367 */ 28368 28369 /** 28370 * This class manages the focus/blur state of the editor. This class is needed since some 28371 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar. 28372 * 28373 * This class will fire two events focus and blur on the editor instances that got affected. 28374 * It will also handle the restore of selection when the focus is lost and returned. 28375 * 28376 * @class tinymce.FocusManager 28377 */ 28378 define("tinymce/FocusManager", [ 28379 "tinymce/dom/DOMUtils", 28380 "tinymce/Env" 28381 ], function(DOMUtils, Env) { 28382 var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; 28383 28384 /** 28385 * Constructs a new focus manager instance. 28386 * 28387 * @constructor FocusManager 28388 * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for. 28389 */ 28390 function FocusManager(editorManager) { 28391 function getActiveElement() { 28392 try { 28393 return document.activeElement; 28394 } catch (ex) { 28395 // IE sometimes fails to get the activeElement when resizing table 28396 // TODO: Investigate this 28397 return document.body; 28398 } 28399 } 28400 28401 // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object 28402 // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well. 28403 function createBookmark(dom, rng) { 28404 if (rng && rng.startContainer) { 28405 // Verify that the range is within the root of the editor 28406 if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) { 28407 return; 28408 } 28409 28410 return { 28411 startContainer: rng.startContainer, 28412 startOffset: rng.startOffset, 28413 endContainer: rng.endContainer, 28414 endOffset: rng.endOffset 28415 }; 28416 } 28417 28418 return rng; 28419 } 28420 28421 function bookmarkToRng(editor, bookmark) { 28422 var rng; 28423 28424 if (bookmark.startContainer) { 28425 rng = editor.getDoc().createRange(); 28426 rng.setStart(bookmark.startContainer, bookmark.startOffset); 28427 rng.setEnd(bookmark.endContainer, bookmark.endOffset); 28428 } else { 28429 rng = bookmark; 28430 } 28431 28432 return rng; 28433 } 28434 28435 function isUIElement(elm) { 28436 return !!DOM.getParent(elm, FocusManager.isEditorUIElement); 28437 } 28438 28439 function registerEvents(e) { 28440 var editor = e.editor; 28441 28442 editor.on('init', function() { 28443 // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab 28444 if (editor.inline || Env.ie) { 28445 // Use the onbeforedeactivate event when available since it works better see #7023 28446 if ("onbeforedeactivate" in document && Env.ie < 9) { 28447 editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) { 28448 if (e.target != editor.getBody()) { 28449 return; 28450 } 28451 28452 try { 28453 editor.lastRng = editor.selection.getRng(); 28454 } catch (ex) { 28455 // IE throws "Unexcpected call to method or property access" some times so lets ignore it 28456 } 28457 }); 28458 } else { 28459 // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes 28460 editor.on('nodechange mouseup keyup', function(e) { 28461 var node = getActiveElement(); 28462 28463 // Only act on manual nodechanges 28464 if (e.type == 'nodechange' && e.selectionChange) { 28465 return; 28466 } 28467 28468 // IE 11 reports active element as iframe not body of iframe 28469 if (node && node.id == editor.id + '_ifr') { 28470 node = editor.getBody(); 28471 } 28472 28473 if (editor.dom.isChildOf(node, editor.getBody())) { 28474 editor.lastRng = editor.selection.getRng(); 28475 } 28476 }); 28477 } 28478 28479 // Handles the issue with WebKit not retaining selection within inline document 28480 // If the user releases the mouse out side the body since a mouse up event wont occur on the body 28481 if (Env.webkit && !selectionChangeHandler) { 28482 selectionChangeHandler = function() { 28483 var activeEditor = editorManager.activeEditor; 28484 28485 if (activeEditor && activeEditor.selection) { 28486 var rng = activeEditor.selection.getRng(); 28487 28488 // Store when it's non collapsed 28489 if (rng && !rng.collapsed) { 28490 editor.lastRng = rng; 28491 } 28492 } 28493 }; 28494 28495 DOM.bind(document, 'selectionchange', selectionChangeHandler); 28496 } 28497 } 28498 }); 28499 28500 editor.on('setcontent', function() { 28501 editor.lastRng = null; 28502 }); 28503 28504 // Remove last selection bookmark on mousedown see #6305 28505 editor.on('mousedown', function() { 28506 editor.selection.lastFocusBookmark = null; 28507 }); 28508 28509 editor.on('focusin', function() { 28510 var focusedEditor = editorManager.focusedEditor; 28511 28512 if (editor.selection.lastFocusBookmark) { 28513 editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark)); 28514 editor.selection.lastFocusBookmark = null; 28515 } 28516 28517 if (focusedEditor != editor) { 28518 if (focusedEditor) { 28519 focusedEditor.fire('blur', {focusedEditor: editor}); 28520 } 28521 28522 editorManager.setActive(editor); 28523 editorManager.focusedEditor = editor; 28524 editor.fire('focus', {blurredEditor: focusedEditor}); 28525 editor.focus(true); 28526 } 28527 28528 editor.lastRng = null; 28529 }); 28530 28531 editor.on('focusout', function() { 28532 window.setTimeout(function() { 28533 var focusedEditor = editorManager.focusedEditor; 28534 28535 // Still the same editor the the blur was outside any editor UI 28536 if (!isUIElement(getActiveElement()) && focusedEditor == editor) { 28537 editor.fire('blur', {focusedEditor: null}); 28538 editorManager.focusedEditor = null; 28539 28540 // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs 28541 if (editor.selection) { 28542 editor.selection.lastFocusBookmark = null; 28543 } 28544 } 28545 }, 0); 28546 }); 28547 28548 // Check if focus is moved to an element outside the active editor by checking if the target node 28549 // isn't within the body of the activeEditor nor a UI element such as a dialog child control 28550 if (!documentFocusInHandler) { 28551 documentFocusInHandler = function(e) { 28552 var activeEditor = editorManager.activeEditor; 28553 28554 if (activeEditor && e.target.ownerDocument == document) { 28555 // Check to make sure we have a valid selection don't update the bookmark if it's 28556 // a focusin to the body of the editor see #7025 28557 if (activeEditor.selection && e.target != activeEditor.getBody()) { 28558 activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng); 28559 } 28560 28561 // Fire a blur event if the element isn't a UI element 28562 if (e.target != document.body && !isUIElement(e.target) && editorManager.focusedEditor == activeEditor) { 28563 activeEditor.fire('blur', {focusedEditor: null}); 28564 editorManager.focusedEditor = null; 28565 } 28566 } 28567 }; 28568 28569 DOM.bind(document, 'focusin', documentFocusInHandler); 28570 } 28571 28572 // Handle edge case when user starts the selection inside the editor and releases 28573 // the mouse outside the editor producing a new selection. This weird workaround is needed since 28574 // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843 28575 if (editor.inline && !documentMouseUpHandler) { 28576 documentMouseUpHandler = function(e) { 28577 var activeEditor = editorManager.activeEditor; 28578 28579 if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) { 28580 var rng = activeEditor.selection.getRng(); 28581 28582 if (!rng.collapsed) { 28583 activeEditor.lastRng = rng; 28584 } 28585 } 28586 }; 28587 28588 DOM.bind(document, 'mouseup', documentMouseUpHandler); 28589 } 28590 } 28591 28592 function unregisterDocumentEvents(e) { 28593 if (editorManager.focusedEditor == e.editor) { 28594 editorManager.focusedEditor = null; 28595 } 28596 28597 if (!editorManager.activeEditor) { 28598 DOM.unbind(document, 'selectionchange', selectionChangeHandler); 28599 DOM.unbind(document, 'focusin', documentFocusInHandler); 28600 DOM.unbind(document, 'mouseup', documentMouseUpHandler); 28601 selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null; 28602 } 28603 } 28604 28605 editorManager.on('AddEditor', registerEvents); 28606 editorManager.on('RemoveEditor', unregisterDocumentEvents); 28607 } 28608 28609 /** 28610 * Returns true if the specified element is part of the UI for example an button or text input. 28611 * 28612 * @method isEditorUIElement 28613 * @param {Element} elm Element to check if it's part of the UI or not. 28614 * @return {Boolean} True/false state if the element is part of the UI or not. 28615 */ 28616 FocusManager.isEditorUIElement = function(elm) { 28617 // Needs to be converted to string since svg can have focus: #6776 28618 return elm.className.toString().indexOf('mce-') !== -1; 28619 }; 28620 28621 return FocusManager; 28622 }); 28623 28624 // Included from: js/tinymce/classes/EditorManager.js 28625 28626 /** 28627 * EditorManager.js 28628 * 28629 * Copyright, Moxiecode Systems AB 28630 * Released under LGPL License. 28631 * 28632 * License: http://www.tinymce.com/license 28633 * Contributing: http://www.tinymce.com/contributing 28634 */ 28635 28636 /** 28637 * This class used as a factory for manager for tinymce.Editor instances. 28638 * 28639 * @example 28640 * tinymce.EditorManager.init({}); 28641 * 28642 * @class tinymce.EditorManager 28643 * @mixes tinymce.util.Observable 28644 * @static 28645 */ 28646 define("tinymce/EditorManager", [ 28647 "tinymce/Editor", 28648 "tinymce/dom/DomQuery", 28649 "tinymce/dom/DOMUtils", 28650 "tinymce/util/URI", 28651 "tinymce/Env", 28652 "tinymce/util/Tools", 28653 "tinymce/util/Observable", 28654 "tinymce/util/I18n", 28655 "tinymce/FocusManager" 28656 ], function(Editor, DomQuery, DOMUtils, URI, Env, Tools, Observable, I18n, FocusManager) { 28657 var DOM = DOMUtils.DOM; 28658 var explode = Tools.explode, each = Tools.each, extend = Tools.extend; 28659 var instanceCounter = 0, beforeUnloadDelegate, EditorManager; 28660 28661 function removeEditorFromList(editor) { 28662 var editors = EditorManager.editors, removedFromList; 28663 28664 delete editors[editor.id]; 28665 28666 for (var i = 0; i < editors.length; i++) { 28667 if (editors[i] == editor) { 28668 editors.splice(i, 1); 28669 removedFromList = true; 28670 break; 28671 } 28672 } 28673 28674 // Select another editor since the active one was removed 28675 if (EditorManager.activeEditor == editor) { 28676 EditorManager.activeEditor = editors[0]; 28677 } 28678 28679 // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor 28680 if (EditorManager.focusedEditor == editor) { 28681 EditorManager.focusedEditor = null; 28682 } 28683 28684 return removedFromList; 28685 } 28686 28687 function purgeDestroyedEditor(editor) { 28688 // User has manually destroyed the editor lets clean up the mess 28689 if (editor && !(editor.getContainer() || editor.getBody()).parentNode) { 28690 removeEditorFromList(editor); 28691 editor.unbindAllNativeEvents(); 28692 editor.destroy(true); 28693 editor = null; 28694 } 28695 28696 return editor; 28697 } 28698 28699 EditorManager = { 28700 /** 28701 * Dom query instance. 28702 * 28703 * @property $ 28704 * @type tinymce.dom.DomQuery 28705 */ 28706 $: DomQuery, 28707 28708 /** 28709 * Major version of TinyMCE build. 28710 * 28711 * @property majorVersion 28712 * @type String 28713 */ 28714 majorVersion: '4', 28715 28716 /** 28717 * Minor version of TinyMCE build. 28718 * 28719 * @property minorVersion 28720 * @type String 28721 */ 28722 minorVersion: '1.6', 28723 28724 /** 28725 * Release date of TinyMCE build. 28726 * 28727 * @property releaseDate 28728 * @type String 28729 */ 28730 releaseDate: '2014-10-08', 28731 28732 /** 28733 * Collection of editor instances. 28734 * 28735 * @property editors 28736 * @type Object 28737 * @example 28738 * for (edId in tinymce.editors) 28739 * tinymce.editors[edId].save(); 28740 */ 28741 editors: [], 28742 28743 /** 28744 * Collection of language pack data. 28745 * 28746 * @property i18n 28747 * @type Object 28748 */ 28749 i18n: I18n, 28750 28751 /** 28752 * Currently active editor instance. 28753 * 28754 * @property activeEditor 28755 * @type tinymce.Editor 28756 * @example 28757 * tinyMCE.activeEditor.selection.getContent(); 28758 * tinymce.EditorManager.activeEditor.selection.getContent(); 28759 */ 28760 activeEditor: null, 28761 28762 setup: function() { 28763 var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; 28764 28765 // Get base URL for the current document 28766 documentBaseURL = document.location.href; 28767 28768 // Check if the URL is a document based format like: http://site/dir/file and file:/// 28769 // leave other formats like applewebdata://... intact 28770 if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) { 28771 documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 28772 28773 if (!/[\/\\]$/.test(documentBaseURL)) { 28774 documentBaseURL += '/'; 28775 } 28776 } 28777 28778 // If tinymce is defined and has a base use that or use the old tinyMCEPreInit 28779 preInit = window.tinymce || window.tinyMCEPreInit; 28780 if (preInit) { 28781 baseURL = preInit.base || preInit.baseURL; 28782 suffix = preInit.suffix; 28783 } else { 28784 // Get base where the tinymce script is located 28785 var scripts = document.getElementsByTagName('script'); 28786 for (var i = 0; i < scripts.length; i++) { 28787 src = scripts[i].src; 28788 28789 // Script types supported: 28790 // tinymce.js tinymce.min.js tinymce.dev.js 28791 // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js 28792 // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js 28793 if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) { 28794 if (src.indexOf('.min') != -1) { 28795 suffix = '.min'; 28796 } 28797 28798 baseURL = src.substring(0, src.lastIndexOf('/')); 28799 break; 28800 } 28801 } 28802 28803 // We didn't find any baseURL by looking at the script elements 28804 // Try to use the document.currentScript as a fallback 28805 if (!baseURL && document.currentScript) { 28806 src = document.currentScript.src; 28807 28808 if (src.indexOf('.min') != -1) { 28809 suffix = '.min'; 28810 } 28811 28812 baseURL = src.substring(0, src.lastIndexOf('/')); 28813 } 28814 } 28815 28816 /** 28817 * Base URL where the root directory if TinyMCE is located. 28818 * 28819 * @property baseURL 28820 * @type String 28821 */ 28822 self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL); 28823 28824 /** 28825 * Document base URL where the current document is located. 28826 * 28827 * @property documentBaseURL 28828 * @type String 28829 */ 28830 self.documentBaseURL = documentBaseURL; 28831 28832 /** 28833 * Absolute baseURI for the installation path of TinyMCE. 28834 * 28835 * @property baseURI 28836 * @type tinymce.util.URI 28837 */ 28838 self.baseURI = new URI(self.baseURL); 28839 28840 /** 28841 * Current suffix to add to each plugin/theme that gets loaded for example ".min". 28842 * 28843 * @property suffix 28844 * @type String 28845 */ 28846 self.suffix = suffix; 28847 28848 self.focusManager = new FocusManager(self); 28849 }, 28850 28851 /** 28852 * Initializes a set of editors. This method will create editors based on various settings. 28853 * 28854 * @method init 28855 * @param {Object} settings Settings object to be passed to each editor instance. 28856 * @example 28857 * // Initializes a editor using the longer method 28858 * tinymce.EditorManager.init({ 28859 * some_settings : 'some value' 28860 * }); 28861 * 28862 * // Initializes a editor instance using the shorter version 28863 * tinyMCE.init({ 28864 * some_settings : 'some value' 28865 * }); 28866 */ 28867 init: function(settings) { 28868 var self = this, editors = []; 28869 28870 function createId(elm) { 28871 var id = elm.id; 28872 28873 // Use element id, or unique name or generate a unique id 28874 if (!id) { 28875 id = elm.name; 28876 28877 if (id && !DOM.get(id)) { 28878 id = elm.name; 28879 } else { 28880 // Generate unique name 28881 id = DOM.uniqueId(); 28882 } 28883 28884 elm.setAttribute('id', id); 28885 } 28886 28887 return id; 28888 } 28889 28890 function createEditor(id, settings, targetElm) { 28891 if (!purgeDestroyedEditor(self.get(id))) { 28892 var editor = new Editor(id, settings, self); 28893 28894 editor.targetElm = editor.targetElm || targetElm; 28895 editors.push(editor); 28896 editor.render(); 28897 } 28898 } 28899 28900 function execCallback(name) { 28901 var callback = settings[name]; 28902 28903 if (!callback) { 28904 return; 28905 } 28906 28907 return callback.apply(self, Array.prototype.slice.call(arguments, 2)); 28908 } 28909 28910 function hasClass(elm, className) { 28911 return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className); 28912 } 28913 28914 function readyHandler() { 28915 var l, co; 28916 28917 DOM.unbind(window, 'ready', readyHandler); 28918 28919 execCallback('onpageload'); 28920 28921 if (settings.types) { 28922 // Process type specific selector 28923 each(settings.types, function(type) { 28924 each(DOM.select(type.selector), function(elm) { 28925 createEditor(createId(elm), extend({}, settings, type), elm); 28926 }); 28927 }); 28928 28929 return; 28930 } else if (settings.selector) { 28931 // Process global selector 28932 each(DOM.select(settings.selector), function(elm) { 28933 createEditor(createId(elm), settings, elm); 28934 }); 28935 28936 return; 28937 } else if (settings.target) { 28938 createEditor(createId(settings.target), settings); 28939 } 28940 28941 // Fallback to old setting 28942 switch (settings.mode) { 28943 case "exact": 28944 l = settings.elements || ''; 28945 28946 if (l.length > 0) { 28947 each(explode(l), function(id) { 28948 var elm; 28949 28950 if ((elm = DOM.get(id))) { 28951 createEditor(id, settings, elm); 28952 } else { 28953 each(document.forms, function(f) { 28954 each(f.elements, function(e) { 28955 if (e.name === id) { 28956 id = 'mce_editor_' + instanceCounter++; 28957 DOM.setAttrib(e, 'id', id); 28958 createEditor(id, settings, e); 28959 } 28960 }); 28961 }); 28962 } 28963 }); 28964 } 28965 break; 28966 28967 case "textareas": 28968 case "specific_textareas": 28969 each(DOM.select('textarea'), function(elm) { 28970 if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) { 28971 return; 28972 } 28973 28974 if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) { 28975 createEditor(createId(elm), settings, elm); 28976 } 28977 }); 28978 break; 28979 } 28980 28981 // Call onInit when all editors are initialized 28982 if (settings.oninit) { 28983 l = co = 0; 28984 28985 each(editors, function(ed) { 28986 co++; 28987 28988 if (!ed.initialized) { 28989 // Wait for it 28990 ed.on('init', function() { 28991 l++; 28992 28993 // All done 28994 if (l == co) { 28995 execCallback('oninit'); 28996 } 28997 }); 28998 } else { 28999 l++; 29000 } 29001 29002 // All done 29003 if (l == co) { 29004 execCallback('oninit'); 29005 } 29006 }); 29007 } 29008 } 29009 29010 self.settings = settings; 29011 29012 DOM.bind(window, 'ready', readyHandler); 29013 }, 29014 29015 /** 29016 * Returns a editor instance by id. 29017 * 29018 * @method get 29019 * @param {String/Number} id Editor instance id or index to return. 29020 * @return {tinymce.Editor} Editor instance to return. 29021 * @example 29022 * // Adds an onclick event to an editor by id (shorter version) 29023 * tinymce.get('mytextbox').on('click', function(e) { 29024 * ed.windowManager.alert('Hello world!'); 29025 * }); 29026 * 29027 * // Adds an onclick event to an editor by id (longer version) 29028 * tinymce.EditorManager.get('mytextbox').on('click', function(e) { 29029 * ed.windowManager.alert('Hello world!'); 29030 * }); 29031 */ 29032 get: function(id) { 29033 if (!arguments.length) { 29034 return this.editors; 29035 } 29036 29037 return id in this.editors ? this.editors[id] : null; 29038 }, 29039 29040 /** 29041 * Adds an editor instance to the editor collection. This will also set it as the active editor. 29042 * 29043 * @method add 29044 * @param {tinymce.Editor} editor Editor instance to add to the collection. 29045 * @return {tinymce.Editor} The same instance that got passed in. 29046 */ 29047 add: function(editor) { 29048 var self = this, editors = self.editors; 29049 29050 // Add named and index editor instance 29051 editors[editor.id] = editor; 29052 editors.push(editor); 29053 29054 // Doesn't call setActive method since we don't want 29055 // to fire a bunch of activate/deactivate calls while initializing 29056 self.activeEditor = editor; 29057 29058 /** 29059 * Fires when an editor is added to the EditorManager collection. 29060 * 29061 * @event AddEditor 29062 * @param {Object} e Event arguments. 29063 */ 29064 self.fire('AddEditor', {editor: editor}); 29065 29066 if (!beforeUnloadDelegate) { 29067 beforeUnloadDelegate = function() { 29068 self.fire('BeforeUnload'); 29069 }; 29070 29071 DOM.bind(window, 'beforeunload', beforeUnloadDelegate); 29072 } 29073 29074 return editor; 29075 }, 29076 29077 /** 29078 * Creates an editor instance and adds it to the EditorManager collection. 29079 * 29080 * @method createEditor 29081 * @param {String} id Instance id to use for editor. 29082 * @param {Object} settings Editor instance settings. 29083 * @return {tinymce.Editor} Editor instance that got created. 29084 */ 29085 createEditor: function(id, settings) { 29086 return this.add(new Editor(id, settings, this)); 29087 }, 29088 29089 /** 29090 * Removes a editor or editors form page. 29091 * 29092 * @example 29093 * // Remove all editors bound to divs 29094 * tinymce.remove('div'); 29095 * 29096 * // Remove all editors bound to textareas 29097 * tinymce.remove('textarea'); 29098 * 29099 * // Remove all editors 29100 * tinymce.remove(); 29101 * 29102 * // Remove specific instance by id 29103 * tinymce.remove('#id'); 29104 * 29105 * @method remove 29106 * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove. 29107 * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null. 29108 */ 29109 remove: function(selector) { 29110 var self = this, i, editors = self.editors, editor; 29111 29112 // Remove all editors 29113 if (!selector) { 29114 for (i = editors.length - 1; i >= 0; i--) { 29115 self.remove(editors[i]); 29116 } 29117 29118 return; 29119 } 29120 29121 // Remove editors by selector 29122 if (typeof(selector) == "string") { 29123 selector = selector.selector || selector; 29124 29125 each(DOM.select(selector), function(elm) { 29126 editor = editors[elm.id]; 29127 29128 if (editor) { 29129 self.remove(editor); 29130 } 29131 }); 29132 29133 return; 29134 } 29135 29136 // Remove specific editor 29137 editor = selector; 29138 29139 // Not in the collection 29140 if (!editors[editor.id]) { 29141 return null; 29142 } 29143 29144 /** 29145 * Fires when an editor is removed from EditorManager collection. 29146 * 29147 * @event RemoveEditor 29148 * @param {Object} e Event arguments. 29149 */ 29150 if (removeEditorFromList(editor)) { 29151 self.fire('RemoveEditor', {editor: editor}); 29152 } 29153 29154 if (!editors.length) { 29155 DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); 29156 } 29157 29158 editor.remove(); 29159 29160 return editor; 29161 }, 29162 29163 /** 29164 * Executes a specific command on the currently active editor. 29165 * 29166 * @method execCommand 29167 * @param {String} c Command to perform for example Bold. 29168 * @param {Boolean} u Optional boolean state if a UI should be presented for the command or not. 29169 * @param {String} v Optional value parameter like for example an URL to a link. 29170 * @return {Boolean} true/false if the command was executed or not. 29171 */ 29172 execCommand: function(cmd, ui, value) { 29173 var self = this, editor = self.get(value); 29174 29175 // Manager commands 29176 switch (cmd) { 29177 case "mceAddEditor": 29178 if (!self.get(value)) { 29179 new Editor(value, self.settings, self).render(); 29180 } 29181 29182 return true; 29183 29184 case "mceRemoveEditor": 29185 if (editor) { 29186 editor.remove(); 29187 } 29188 29189 return true; 29190 29191 case 'mceToggleEditor': 29192 if (!editor) { 29193 self.execCommand('mceAddEditor', 0, value); 29194 return true; 29195 } 29196 29197 if (editor.isHidden()) { 29198 editor.show(); 29199 } else { 29200 editor.hide(); 29201 } 29202 29203 return true; 29204 } 29205 29206 // Run command on active editor 29207 if (self.activeEditor) { 29208 return self.activeEditor.execCommand(cmd, ui, value); 29209 } 29210 29211 return false; 29212 }, 29213 29214 /** 29215 * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted. 29216 * 29217 * @method triggerSave 29218 * @example 29219 * // Saves all contents 29220 * tinyMCE.triggerSave(); 29221 */ 29222 triggerSave: function() { 29223 each(this.editors, function(editor) { 29224 editor.save(); 29225 }); 29226 }, 29227 29228 /** 29229 * Adds a language pack, this gets called by the loaded language files like en.js. 29230 * 29231 * @method addI18n 29232 * @param {String} code Optional language code. 29233 * @param {Object} items Name/value object with translations. 29234 */ 29235 addI18n: function(code, items) { 29236 I18n.add(code, items); 29237 }, 29238 29239 /** 29240 * Translates the specified string using the language pack items. 29241 * 29242 * @method translate 29243 * @param {String/Array/Object} text String to translate 29244 * @return {String} Translated string. 29245 */ 29246 translate: function(text) { 29247 return I18n.translate(text); 29248 }, 29249 29250 /** 29251 * Sets the active editor instance and fires the deactivate/activate events. 29252 * 29253 * @method setActive 29254 * @param {tinymce.Editor} editor Editor instance to set as the active instance. 29255 */ 29256 setActive: function(editor) { 29257 var activeEditor = this.activeEditor; 29258 29259 if (this.activeEditor != editor) { 29260 if (activeEditor) { 29261 activeEditor.fire('deactivate', {relatedTarget: editor}); 29262 } 29263 29264 editor.fire('activate', {relatedTarget: activeEditor}); 29265 } 29266 29267 this.activeEditor = editor; 29268 } 29269 }; 29270 29271 extend(EditorManager, Observable); 29272 29273 EditorManager.setup(); 29274 29275 // Export EditorManager as tinymce/tinymce in global namespace 29276 window.tinymce = window.tinyMCE = EditorManager; 29277 29278 return EditorManager; 29279 }); 29280 29281 // Included from: js/tinymce/classes/LegacyInput.js 29282 29283 /** 29284 * LegacyInput.js 29285 * 29286 * Copyright, Moxiecode Systems AB 29287 * Released under LGPL License. 29288 * 29289 * License: http://www.tinymce.com/license 29290 * Contributing: http://www.tinymce.com/contributing 29291 */ 29292 29293 define("tinymce/LegacyInput", [ 29294 "tinymce/EditorManager", 29295 "tinymce/util/Tools" 29296 ], function(EditorManager, Tools) { 29297 var each = Tools.each, explode = Tools.explode; 29298 29299 EditorManager.on('AddEditor', function(e) { 29300 var editor = e.editor; 29301 29302 editor.on('preInit', function() { 29303 var filters, fontSizes, dom, settings = editor.settings; 29304 29305 function replaceWithSpan(node, styles) { 29306 each(styles, function(value, name) { 29307 if (value) { 29308 dom.setStyle(node, name, value); 29309 } 29310 }); 29311 29312 dom.rename(node, 'span'); 29313 } 29314 29315 function convert(e) { 29316 dom = editor.dom; 29317 29318 if (settings.convert_fonts_to_spans) { 29319 each(dom.select('font,u,strike', e.node), function(node) { 29320 filters[node.nodeName.toLowerCase()](dom, node); 29321 }); 29322 } 29323 } 29324 29325 if (settings.inline_styles) { 29326 fontSizes = explode(settings.font_size_legacy_values); 29327 29328 filters = { 29329 font: function(dom, node) { 29330 replaceWithSpan(node, { 29331 backgroundColor: node.style.backgroundColor, 29332 color: node.color, 29333 fontFamily: node.face, 29334 fontSize: fontSizes[parseInt(node.size, 10) - 1] 29335 }); 29336 }, 29337 29338 u: function(dom, node) { 29339 replaceWithSpan(node, { 29340 textDecoration: 'underline' 29341 }); 29342 }, 29343 29344 strike: function(dom, node) { 29345 replaceWithSpan(node, { 29346 textDecoration: 'line-through' 29347 }); 29348 } 29349 }; 29350 29351 editor.on('PreProcess SetContent', convert); 29352 } 29353 }); 29354 }); 29355 }); 29356 29357 // Included from: js/tinymce/classes/util/XHR.js 29358 29359 /** 29360 * XHR.js 29361 * 29362 * Copyright, Moxiecode Systems AB 29363 * Released under LGPL License. 29364 * 29365 * License: http://www.tinymce.com/license 29366 * Contributing: http://www.tinymce.com/contributing 29367 */ 29368 29369 /** 29370 * This class enables you to send XMLHTTPRequests cross browser. 29371 * @class tinymce.util.XHR 29372 * @mixes tinymce.util.Observable 29373 * @static 29374 * @example 29375 * // Sends a low level Ajax request 29376 * tinymce.util.XHR.send({ 29377 * url: 'someurl', 29378 * success: function(text) { 29379 * console.debug(text); 29380 * } 29381 * }); 29382 * 29383 * // Add custom header to XHR request 29384 * tinymce.util.XHR.on('beforeSend', function(e) { 29385 * e.xhr.setRequestHeader('X-Requested-With', 'Something'); 29386 * }); 29387 */ 29388 define("tinymce/util/XHR", [ 29389 "tinymce/util/Observable", 29390 "tinymce/util/Tools" 29391 ], function(Observable, Tools) { 29392 var XHR = { 29393 /** 29394 * Sends a XMLHTTPRequest. 29395 * Consult the Wiki for details on what settings this method takes. 29396 * 29397 * @method send 29398 * @param {Object} settings Object will target URL, callbacks and other info needed to make the request. 29399 */ 29400 send: function(settings) { 29401 var xhr, count = 0; 29402 29403 function ready() { 29404 if (!settings.async || xhr.readyState == 4 || count++ > 10000) { 29405 if (settings.success && count < 10000 && xhr.status == 200) { 29406 settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings); 29407 } else if (settings.error) { 29408 settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings); 29409 } 29410 29411 xhr = null; 29412 } else { 29413 setTimeout(ready, 10); 29414 } 29415 } 29416 29417 // Default settings 29418 settings.scope = settings.scope || this; 29419 settings.success_scope = settings.success_scope || settings.scope; 29420 settings.error_scope = settings.error_scope || settings.scope; 29421 settings.async = settings.async === false ? false : true; 29422 settings.data = settings.data || ''; 29423 29424 xhr = new XMLHttpRequest(); 29425 29426 if (xhr) { 29427 if (xhr.overrideMimeType) { 29428 xhr.overrideMimeType(settings.content_type); 29429 } 29430 29431 xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async); 29432 29433 if (settings.crossDomain) { 29434 xhr.withCredentials = true; 29435 } 29436 29437 if (settings.content_type) { 29438 xhr.setRequestHeader('Content-Type', settings.content_type); 29439 } 29440 29441 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 29442 29443 xhr = XHR.fire('beforeSend', {xhr: xhr, settings: settings}).xhr; 29444 xhr.send(settings.data); 29445 29446 // Syncronous request 29447 if (!settings.async) { 29448 return ready(); 29449 } 29450 29451 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE 29452 setTimeout(ready, 10); 29453 } 29454 } 29455 }; 29456 29457 Tools.extend(XHR, Observable); 29458 29459 return XHR; 29460 }); 29461 29462 // Included from: js/tinymce/classes/util/JSON.js 29463 29464 /** 29465 * JSON.js 29466 * 29467 * Copyright, Moxiecode Systems AB 29468 * Released under LGPL License. 29469 * 29470 * License: http://www.tinymce.com/license 29471 * Contributing: http://www.tinymce.com/contributing 29472 */ 29473 29474 /** 29475 * JSON parser and serializer class. 29476 * 29477 * @class tinymce.util.JSON 29478 * @static 29479 * @example 29480 * // JSON parse a string into an object 29481 * var obj = tinymce.util.JSON.parse(somestring); 29482 * 29483 * // JSON serialize a object into an string 29484 * var str = tinymce.util.JSON.serialize(obj); 29485 */ 29486 define("tinymce/util/JSON", [], function() { 29487 function serialize(o, quote) { 29488 var i, v, t, name; 29489 29490 quote = quote || '"'; 29491 29492 if (o === null) { 29493 return 'null'; 29494 } 29495 29496 t = typeof o; 29497 29498 if (t == 'string') { 29499 v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; 29500 29501 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { 29502 // Make sure single quotes never get encoded inside double quotes for JSON compatibility 29503 if (quote === '"' && a === "'") { 29504 return a; 29505 } 29506 29507 i = v.indexOf(b); 29508 29509 if (i + 1) { 29510 return '\\' + v.charAt(i + 1); 29511 } 29512 29513 a = b.charCodeAt().toString(16); 29514 29515 return '\\u' + '0000'.substring(a.length) + a; 29516 }) + quote; 29517 } 29518 29519 if (t == 'object') { 29520 if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') { 29521 for (i = 0, v = '['; i < o.length; i++) { 29522 v += (i > 0 ? ',' : '') + serialize(o[i], quote); 29523 } 29524 29525 return v + ']'; 29526 } 29527 29528 v = '{'; 29529 29530 for (name in o) { 29531 if (o.hasOwnProperty(name)) { 29532 v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + 29533 quote + ':' + serialize(o[name], quote) : ''; 29534 } 29535 } 29536 29537 return v + '}'; 29538 } 29539 29540 return '' + o; 29541 } 29542 29543 return { 29544 /** 29545 * Serializes the specified object as a JSON string. 29546 * 29547 * @method serialize 29548 * @param {Object} obj Object to serialize as a JSON string. 29549 * @param {String} quote Optional quote string defaults to ". 29550 * @return {string} JSON string serialized from input. 29551 */ 29552 serialize: serialize, 29553 29554 /** 29555 * Unserializes/parses the specified JSON string into a object. 29556 * 29557 * @method parse 29558 * @param {string} s JSON String to parse into a JavaScript object. 29559 * @return {Object} Object from input JSON string or undefined if it failed. 29560 */ 29561 parse: function(text) { 29562 try { 29563 // Trick uglify JS 29564 return window[String.fromCharCode(101) + 'val']('(' + text + ')'); 29565 } catch (ex) { 29566 // Ignore 29567 } 29568 } 29569 29570 /**#@-*/ 29571 }; 29572 }); 29573 29574 // Included from: js/tinymce/classes/util/JSONRequest.js 29575 29576 /** 29577 * JSONRequest.js 29578 * 29579 * Copyright, Moxiecode Systems AB 29580 * Released under LGPL License. 29581 * 29582 * License: http://www.tinymce.com/license 29583 * Contributing: http://www.tinymce.com/contributing 29584 */ 29585 29586 /** 29587 * This class enables you to use JSON-RPC to call backend methods. 29588 * 29589 * @class tinymce.util.JSONRequest 29590 * @example 29591 * var json = new tinymce.util.JSONRequest({ 29592 * url: 'somebackend.php' 29593 * }); 29594 * 29595 * // Send RPC call 1 29596 * json.send({ 29597 * method: 'someMethod1', 29598 * params: ['a', 'b'], 29599 * success: function(result) { 29600 * console.dir(result); 29601 * } 29602 * }); 29603 * 29604 * // Send RPC call 2 29605 * json.send({ 29606 * method: 'someMethod2', 29607 * params: ['a', 'b'], 29608 * success: function(result) { 29609 * console.dir(result); 29610 * } 29611 * }); 29612 */ 29613 define("tinymce/util/JSONRequest", [ 29614 "tinymce/util/JSON", 29615 "tinymce/util/XHR", 29616 "tinymce/util/Tools" 29617 ], function(JSON, XHR, Tools) { 29618 var extend = Tools.extend; 29619 29620 function JSONRequest(settings) { 29621 this.settings = extend({}, settings); 29622 this.count = 0; 29623 } 29624 29625 /** 29626 * Simple helper function to send a JSON-RPC request without the need to initialize an object. 29627 * Consult the Wiki API documentation for more details on what you can pass to this function. 29628 * 29629 * @method sendRPC 29630 * @static 29631 * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc. 29632 */ 29633 JSONRequest.sendRPC = function(o) { 29634 return new JSONRequest().send(o); 29635 }; 29636 29637 JSONRequest.prototype = { 29638 /** 29639 * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function. 29640 * 29641 * @method send 29642 * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc. 29643 */ 29644 send: function(args) { 29645 var ecb = args.error, scb = args.success; 29646 29647 args = extend(this.settings, args); 29648 29649 args.success = function(c, x) { 29650 c = JSON.parse(c); 29651 29652 if (typeof(c) == 'undefined') { 29653 c = { 29654 error : 'JSON Parse error.' 29655 }; 29656 } 29657 29658 if (c.error) { 29659 ecb.call(args.error_scope || args.scope, c.error, x); 29660 } else { 29661 scb.call(args.success_scope || args.scope, c.result); 29662 } 29663 }; 29664 29665 args.error = function(ty, x) { 29666 if (ecb) { 29667 ecb.call(args.error_scope || args.scope, ty, x); 29668 } 29669 }; 29670 29671 args.data = JSON.serialize({ 29672 id: args.id || 'c' + (this.count++), 29673 method: args.method, 29674 params: args.params 29675 }); 29676 29677 // JSON content type for Ruby on rails. Bug: #1883287 29678 args.content_type = 'application/json'; 29679 29680 XHR.send(args); 29681 } 29682 }; 29683 29684 return JSONRequest; 29685 }); 29686 29687 // Included from: js/tinymce/classes/util/JSONP.js 29688 29689 /** 29690 * JSONP.js 29691 * 29692 * Copyright, Moxiecode Systems AB 29693 * Released under LGPL License. 29694 * 29695 * License: http://www.tinymce.com/license 29696 * Contributing: http://www.tinymce.com/contributing 29697 */ 29698 29699 define("tinymce/util/JSONP", [ 29700 "tinymce/dom/DOMUtils" 29701 ], function(DOMUtils) { 29702 return { 29703 callbacks: {}, 29704 count: 0, 29705 29706 send: function(settings) { 29707 var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count; 29708 var id = 'tinymce_jsonp_' + count; 29709 29710 self.callbacks[count] = function(json) { 29711 dom.remove(id); 29712 delete self.callbacks[count]; 29713 29714 settings.callback(json); 29715 }; 29716 29717 dom.add(dom.doc.body, 'script', { 29718 id: id, 29719 src: settings.url, 29720 type: 'text/javascript' 29721 }); 29722 29723 self.count++; 29724 } 29725 }; 29726 }); 29727 29728 // Included from: js/tinymce/classes/util/LocalStorage.js 29729 29730 /** 29731 * LocalStorage.js 29732 * 29733 * Copyright, Moxiecode Systems AB 29734 * Released under LGPL License. 29735 * 29736 * License: http://www.tinymce.com/license 29737 * Contributing: http://www.tinymce.com/contributing 29738 */ 29739 29740 /** 29741 * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers. 29742 * Storage is done using userData on IE 7 and a special serialization format. The format is designed 29743 * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This 29744 * makes it possible to store for example HTML data. 29745 * 29746 * Storage format for userData: 29747 * <base 32 key length>,<key string>,<base 32 value length>,<value>,... 29748 * 29749 * For example this data key1=value1,key2=value2 would be: 29750 * 4,key1,6,value1,4,key2,6,value2 29751 * 29752 * @class tinymce.util.LocalStorage 29753 * @static 29754 * @version 4.0 29755 * @example 29756 * tinymce.util.LocalStorage.setItem('key', 'value'); 29757 * var value = tinymce.util.LocalStorage.getItem('key'); 29758 */ 29759 define("tinymce/util/LocalStorage", [], function() { 29760 var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport; 29761 29762 // Check for native support 29763 try { 29764 if (window.localStorage) { 29765 return localStorage; 29766 } 29767 } catch (ex) { 29768 // Ignore 29769 } 29770 29771 userDataKey = "tinymce"; 29772 storageElm = document.documentElement; 29773 hasOldIEDataSupport = !!storageElm.addBehavior; 29774 29775 if (hasOldIEDataSupport) { 29776 storageElm.addBehavior('#default#userData'); 29777 } 29778 29779 /** 29780 * Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters. 29781 */ 29782 function updateKeys() { 29783 keys = []; 29784 29785 for (var key in items) { 29786 keys.push(key); 29787 } 29788 29789 LocalStorage.length = keys.length; 29790 } 29791 29792 /** 29793 * Loads the userData string and parses it into the items structure. 29794 */ 29795 function load() { 29796 var key, data, value, pos = 0; 29797 29798 items = {}; 29799 29800 // localStorage can be disabled on WebKit/Gecko so make a dummy storage 29801 if (!hasOldIEDataSupport) { 29802 return; 29803 } 29804 29805 function next(end) { 29806 var value, nextPos; 29807 29808 nextPos = end !== undefined ? pos + end : data.indexOf(',', pos); 29809 if (nextPos === -1 || nextPos > data.length) { 29810 return null; 29811 } 29812 29813 value = data.substring(pos, nextPos); 29814 pos = nextPos + 1; 29815 29816 return value; 29817 } 29818 29819 storageElm.load(userDataKey); 29820 data = storageElm.getAttribute(userDataKey) || ''; 29821 29822 do { 29823 var offset = next(); 29824 if (offset === null) { 29825 break; 29826 } 29827 29828 key = next(parseInt(offset, 32) || 0); 29829 if (key !== null) { 29830 offset = next(); 29831 if (offset === null) { 29832 break; 29833 } 29834 29835 value = next(parseInt(offset, 32) || 0); 29836 29837 if (key) { 29838 items[key] = value; 29839 } 29840 } 29841 } while (key !== null); 29842 29843 updateKeys(); 29844 } 29845 29846 /** 29847 * Saves the items structure into a the userData format. 29848 */ 29849 function save() { 29850 var value, data = ''; 29851 29852 // localStorage can be disabled on WebKit/Gecko so make a dummy storage 29853 if (!hasOldIEDataSupport) { 29854 return; 29855 } 29856 29857 for (var key in items) { 29858 value = items[key]; 29859 data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value; 29860 } 29861 29862 storageElm.setAttribute(userDataKey, data); 29863 29864 try { 29865 storageElm.save(userDataKey); 29866 } catch (ex) { 29867 // Ignore disk full 29868 } 29869 29870 updateKeys(); 29871 } 29872 29873 LocalStorage = { 29874 /** 29875 * Length of the number of items in storage. 29876 * 29877 * @property length 29878 * @type Number 29879 * @return {Number} Number of items in storage. 29880 */ 29881 //length:0, 29882 29883 /** 29884 * Returns the key name by index. 29885 * 29886 * @method key 29887 * @param {Number} index Index of key to return. 29888 * @return {String} Key value or null if it wasn't found. 29889 */ 29890 key: function(index) { 29891 return keys[index]; 29892 }, 29893 29894 /** 29895 * Returns the value if the specified key or null if it wasn't found. 29896 * 29897 * @method getItem 29898 * @param {String} key Key of item to retrive. 29899 * @return {String} Value of the specified item or null if it wasn't found. 29900 */ 29901 getItem: function(key) { 29902 return key in items ? items[key] : null; 29903 }, 29904 29905 /** 29906 * Sets the value of the specified item by it's key. 29907 * 29908 * @method setItem 29909 * @param {String} key Key of the item to set. 29910 * @param {String} value Value of the item to set. 29911 */ 29912 setItem: function(key, value) { 29913 items[key] = "" + value; 29914 save(); 29915 }, 29916 29917 /** 29918 * Removes the specified item by key. 29919 * 29920 * @method removeItem 29921 * @param {String} key Key of item to remove. 29922 */ 29923 removeItem: function(key) { 29924 delete items[key]; 29925 save(); 29926 }, 29927 29928 /** 29929 * Removes all items. 29930 * 29931 * @method clear 29932 */ 29933 clear: function() { 29934 items = {}; 29935 save(); 29936 } 29937 }; 29938 29939 load(); 29940 29941 return LocalStorage; 29942 }); 29943 29944 // Included from: js/tinymce/classes/Compat.js 29945 29946 /** 29947 * Compat.js 29948 * 29949 * Copyright, Moxiecode Systems AB 29950 * Released under LGPL License. 29951 * 29952 * License: http://www.tinymce.com/license 29953 * Contributing: http://www.tinymce.com/contributing 29954 */ 29955 29956 /** 29957 * TinyMCE core class. 29958 * 29959 * @static 29960 * @class tinymce 29961 * @borrow-members tinymce.EditorManager 29962 * @borrow-members tinymce.util.Tools 29963 */ 29964 define("tinymce/Compat", [ 29965 "tinymce/dom/DOMUtils", 29966 "tinymce/dom/EventUtils", 29967 "tinymce/dom/ScriptLoader", 29968 "tinymce/AddOnManager", 29969 "tinymce/util/Tools", 29970 "tinymce/Env" 29971 ], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) { 29972 var tinymce = window.tinymce; 29973 29974 /** 29975 * @property {tinymce.dom.DOMUtils} DOM Global DOM instance. 29976 * @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance. 29977 * @property {tinymce.AddOnManager} PluginManager Global PluginManager instance. 29978 * @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance. 29979 */ 29980 tinymce.DOM = DOMUtils.DOM; 29981 tinymce.ScriptLoader = ScriptLoader.ScriptLoader; 29982 tinymce.PluginManager = AddOnManager.PluginManager; 29983 tinymce.ThemeManager = AddOnManager.ThemeManager; 29984 29985 tinymce.dom = tinymce.dom || {}; 29986 tinymce.dom.Event = EventUtils.Event; 29987 29988 Tools.each(Tools, function(func, key) { 29989 tinymce[key] = func; 29990 }); 29991 29992 Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) { 29993 tinymce[name] = Env[name.substr(2).toLowerCase()]; 29994 }); 29995 29996 return {}; 29997 }); 29998 29999 // Describe the different namespaces 30000 30001 /** 30002 * Root level namespace this contains classes directly releated to the TinyMCE editor. 30003 * 30004 * @namespace tinymce 30005 */ 30006 30007 /** 30008 * Contains classes for handling the browsers DOM. 30009 * 30010 * @namespace tinymce.dom 30011 */ 30012 30013 /** 30014 * Contains html parser and serializer logic. 30015 * 30016 * @namespace tinymce.html 30017 */ 30018 30019 /** 30020 * Contains the different UI types such as buttons, listboxes etc. 30021 * 30022 * @namespace tinymce.ui 30023 */ 30024 30025 /** 30026 * Contains various utility classes such as json parser, cookies etc. 30027 * 30028 * @namespace tinymce.util 30029 */ 30030 30031 // Included from: js/tinymce/classes/ui/Layout.js 30032 30033 /** 30034 * Layout.js 30035 * 30036 * Copyright, Moxiecode Systems AB 30037 * Released under LGPL License. 30038 * 30039 * License: http://www.tinymce.com/license 30040 * Contributing: http://www.tinymce.com/contributing 30041 */ 30042 30043 /** 30044 * Base layout manager class. 30045 * 30046 * @class tinymce.ui.Layout 30047 */ 30048 define("tinymce/ui/Layout", [ 30049 "tinymce/util/Class", 30050 "tinymce/util/Tools" 30051 ], function(Class, Tools) { 30052 "use strict"; 30053 30054 return Class.extend({ 30055 Defaults: { 30056 firstControlClass: 'first', 30057 lastControlClass: 'last' 30058 }, 30059 30060 /** 30061 * Constructs a layout instance with the specified settings. 30062 * 30063 * @constructor 30064 * @param {Object} settings Name/value object with settings. 30065 */ 30066 init: function(settings) { 30067 this.settings = Tools.extend({}, this.Defaults, settings); 30068 }, 30069 30070 /** 30071 * This method gets invoked before the layout renders the controls. 30072 * 30073 * @method preRender 30074 * @param {tinymce.ui.Container} container Container instance to preRender. 30075 */ 30076 preRender: function(container) { 30077 container.addClass(this.settings.containerClass, 'body'); 30078 }, 30079 30080 /** 30081 * Applies layout classes to the container. 30082 * 30083 * @private 30084 */ 30085 applyClasses: function(container) { 30086 var self = this, settings = self.settings, items, firstClass, lastClass; 30087 30088 items = container.items().filter(':visible'); 30089 firstClass = settings.firstControlClass; 30090 lastClass = settings.lastControlClass; 30091 30092 items.each(function(item) { 30093 item.removeClass(firstClass).removeClass(lastClass); 30094 30095 if (settings.controlClass) { 30096 item.addClass(settings.controlClass); 30097 } 30098 }); 30099 30100 items.eq(0).addClass(firstClass); 30101 items.eq(-1).addClass(lastClass); 30102 }, 30103 30104 /** 30105 * Renders the specified container and any layout specific HTML. 30106 * 30107 * @method renderHtml 30108 * @param {tinymce.ui.Container} container Container to render HTML for. 30109 */ 30110 renderHtml: function(container) { 30111 var self = this, settings = self.settings, items, html = ''; 30112 30113 items = container.items(); 30114 items.eq(0).addClass(settings.firstControlClass); 30115 items.eq(-1).addClass(settings.lastControlClass); 30116 30117 items.each(function(item) { 30118 if (settings.controlClass) { 30119 item.addClass(settings.controlClass); 30120 } 30121 30122 html += item.renderHtml(); 30123 }); 30124 30125 return html; 30126 }, 30127 30128 /** 30129 * Recalculates the positions of the controls in the specified container. 30130 * 30131 * @method recalc 30132 * @param {tinymce.ui.Container} container Container instance to recalc. 30133 */ 30134 recalc: function() { 30135 }, 30136 30137 /** 30138 * This method gets invoked after the layout renders the controls. 30139 * 30140 * @method postRender 30141 * @param {tinymce.ui.Container} container Container instance to postRender. 30142 */ 30143 postRender: function() { 30144 } 30145 }); 30146 }); 30147 30148 // Included from: js/tinymce/classes/ui/AbsoluteLayout.js 30149 30150 /** 30151 * AbsoluteLayout.js 30152 * 30153 * Copyright, Moxiecode Systems AB 30154 * Released under LGPL License. 30155 * 30156 * License: http://www.tinymce.com/license 30157 * Contributing: http://www.tinymce.com/contributing 30158 */ 30159 30160 /** 30161 * LayoutManager for absolute positioning. This layout manager is more of 30162 * a base class for other layouts but can be created and used directly. 30163 * 30164 * @-x-less AbsoluteLayout.less 30165 * @class tinymce.ui.AbsoluteLayout 30166 * @extends tinymce.ui.Layout 30167 */ 30168 define("tinymce/ui/AbsoluteLayout", [ 30169 "tinymce/ui/Layout" 30170 ], function(Layout) { 30171 "use strict"; 30172 30173 return Layout.extend({ 30174 Defaults: { 30175 containerClass: 'abs-layout', 30176 controlClass: 'abs-layout-item' 30177 }, 30178 30179 /** 30180 * Recalculates the positions of the controls in the specified container. 30181 * 30182 * @method recalc 30183 * @param {tinymce.ui.Container} container Container instance to recalc. 30184 */ 30185 recalc: function(container) { 30186 container.items().filter(':visible').each(function(ctrl) { 30187 var settings = ctrl.settings; 30188 30189 ctrl.layoutRect({ 30190 x: settings.x, 30191 y: settings.y, 30192 w: settings.w, 30193 h: settings.h 30194 }); 30195 30196 if (ctrl.recalc) { 30197 ctrl.recalc(); 30198 } 30199 }); 30200 }, 30201 30202 /** 30203 * Renders the specified container and any layout specific HTML. 30204 * 30205 * @method renderHtml 30206 * @param {tinymce.ui.Container} container Container to render HTML for. 30207 */ 30208 renderHtml: function(container) { 30209 return '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container); 30210 } 30211 }); 30212 }); 30213 30214 // Included from: js/tinymce/classes/ui/Tooltip.js 30215 30216 /** 30217 * Tooltip.js 30218 * 30219 * Copyright, Moxiecode Systems AB 30220 * Released under LGPL License. 30221 * 30222 * License: http://www.tinymce.com/license 30223 * Contributing: http://www.tinymce.com/contributing 30224 */ 30225 30226 /** 30227 * Creates a tooltip instance. 30228 * 30229 * @-x-less ToolTip.less 30230 * @class tinymce.ui.ToolTip 30231 * @extends tinymce.ui.Control 30232 * @mixes tinymce.ui.Movable 30233 */ 30234 define("tinymce/ui/Tooltip", [ 30235 "tinymce/ui/Control", 30236 "tinymce/ui/Movable" 30237 ], function(Control, Movable) { 30238 return Control.extend({ 30239 Mixins: [Movable], 30240 30241 Defaults: { 30242 classes: 'widget tooltip tooltip-n' 30243 }, 30244 30245 /** 30246 * Sets/gets the current label text. 30247 * 30248 * @method text 30249 * @param {String} [text] New label text. 30250 * @return {String|tinymce.ui.Tooltip} Current text or current label instance. 30251 */ 30252 text: function(value) { 30253 var self = this; 30254 30255 if (typeof(value) != "undefined") { 30256 self._value = value; 30257 30258 if (self._rendered) { 30259 self.getEl().lastChild.innerHTML = self.encode(value); 30260 } 30261 30262 return self; 30263 } 30264 30265 return self._value; 30266 }, 30267 30268 /** 30269 * Renders the control as a HTML string. 30270 * 30271 * @method renderHtml 30272 * @return {String} HTML representing the control. 30273 */ 30274 renderHtml: function() { 30275 var self = this, prefix = self.classPrefix; 30276 30277 return ( 30278 '<div id="' + self._id + '" class="' + self.classes() + '" role="presentation">' + 30279 '<div class="' + prefix + 'tooltip-arrow"></div>' + 30280 '<div class="' + prefix + 'tooltip-inner">' + self.encode(self._text) + '</div>' + 30281 '</div>' 30282 ); 30283 }, 30284 30285 /** 30286 * Repaints the control after a layout operation. 30287 * 30288 * @method repaint 30289 */ 30290 repaint: function() { 30291 var self = this, style, rect; 30292 30293 style = self.getEl().style; 30294 rect = self._layoutRect; 30295 30296 style.left = rect.x + 'px'; 30297 style.top = rect.y + 'px'; 30298 style.zIndex = 0xFFFF + 0xFFFF; 30299 } 30300 }); 30301 }); 30302 30303 // Included from: js/tinymce/classes/ui/Widget.js 30304 30305 /** 30306 * Widget.js 30307 * 30308 * Copyright, Moxiecode Systems AB 30309 * Released under LGPL License. 30310 * 30311 * License: http://www.tinymce.com/license 30312 * Contributing: http://www.tinymce.com/contributing 30313 */ 30314 30315 /** 30316 * Widget base class a widget is a control that has a tooltip and some basic states. 30317 * 30318 * @class tinymce.ui.Widget 30319 * @extends tinymce.ui.Control 30320 */ 30321 define("tinymce/ui/Widget", [ 30322 "tinymce/ui/Control", 30323 "tinymce/ui/Tooltip" 30324 ], function(Control, Tooltip) { 30325 "use strict"; 30326 30327 var tooltip; 30328 30329 var Widget = Control.extend({ 30330 /** 30331 * Constructs a instance with the specified settings. 30332 * 30333 * @constructor 30334 * @param {Object} settings Name/value object with settings. 30335 * @setting {String} tooltip Tooltip text to display when hovering. 30336 * @setting {Boolean} autofocus True if the control should be focused when rendered. 30337 * @setting {String} text Text to display inside widget. 30338 */ 30339 init: function(settings) { 30340 var self = this; 30341 30342 self._super(settings); 30343 settings = self.settings; 30344 self.canFocus = true; 30345 30346 if (settings.tooltip && Widget.tooltips !== false) { 30347 self.on('mouseenter', function(e) { 30348 var tooltip = self.tooltip().moveTo(-0xFFFF); 30349 30350 if (e.control == self) { 30351 var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']); 30352 30353 tooltip.toggleClass('tooltip-n', rel == 'bc-tc'); 30354 tooltip.toggleClass('tooltip-nw', rel == 'bc-tl'); 30355 tooltip.toggleClass('tooltip-ne', rel == 'bc-tr'); 30356 30357 tooltip.moveRel(self.getEl(), rel); 30358 } else { 30359 tooltip.hide(); 30360 } 30361 }); 30362 30363 self.on('mouseleave mousedown click', function() { 30364 self.tooltip().hide(); 30365 }); 30366 } 30367 30368 self.aria('label', settings.ariaLabel || settings.tooltip); 30369 }, 30370 30371 /** 30372 * Returns the current tooltip instance. 30373 * 30374 * @method tooltip 30375 * @return {tinymce.ui.Tooltip} Tooltip instance. 30376 */ 30377 tooltip: function() { 30378 if (!tooltip) { 30379 tooltip = new Tooltip({type: 'tooltip'}); 30380 tooltip.renderTo(); 30381 } 30382 30383 return tooltip; 30384 }, 30385 30386 /** 30387 * Sets/gets the active state of the widget. 30388 * 30389 * @method active 30390 * @param {Boolean} [state] State if the control is active. 30391 * @return {Boolean|tinymce.ui.Widget} True/false or current widget instance. 30392 */ 30393 active: function(state) { 30394 var self = this, undef; 30395 30396 if (state !== undef) { 30397 self.aria('pressed', state); 30398 self.toggleClass('active', state); 30399 } 30400 30401 return self._super(state); 30402 }, 30403 30404 /** 30405 * Sets/gets the disabled state of the widget. 30406 * 30407 * @method disabled 30408 * @param {Boolean} [state] State if the control is disabled. 30409 * @return {Boolean|tinymce.ui.Widget} True/false or current widget instance. 30410 */ 30411 disabled: function(state) { 30412 var self = this, undef; 30413 30414 if (state !== undef) { 30415 self.aria('disabled', state); 30416 self.toggleClass('disabled', state); 30417 } 30418 30419 return self._super(state); 30420 }, 30421 30422 /** 30423 * Called after the control has been rendered. 30424 * 30425 * @method postRender 30426 */ 30427 postRender: function() { 30428 var self = this, settings = self.settings; 30429 30430 self._rendered = true; 30431 30432 self._super(); 30433 30434 if (!self.parent() && (settings.width || settings.height)) { 30435 self.initLayoutRect(); 30436 self.repaint(); 30437 } 30438 30439 if (settings.autofocus) { 30440 self.focus(); 30441 } 30442 }, 30443 30444 /** 30445 * Removes the current control from DOM and from UI collections. 30446 * 30447 * @method remove 30448 * @return {tinymce.ui.Control} Current control instance. 30449 */ 30450 remove: function() { 30451 this._super(); 30452 30453 if (tooltip) { 30454 tooltip.remove(); 30455 tooltip = null; 30456 } 30457 } 30458 }); 30459 30460 return Widget; 30461 }); 30462 30463 // Included from: js/tinymce/classes/ui/Button.js 30464 30465 /** 30466 * Button.js 30467 * 30468 * Copyright, Moxiecode Systems AB 30469 * Released under LGPL License. 30470 * 30471 * License: http://www.tinymce.com/license 30472 * Contributing: http://www.tinymce.com/contributing 30473 */ 30474 30475 /** 30476 * This class is used to create buttons. You can create them directly or through the Factory. 30477 * 30478 * @example 30479 * // Create and render a button to the body element 30480 * tinymce.ui.Factory.create({ 30481 * type: 'button', 30482 * text: 'My button' 30483 * }).renderTo(document.body); 30484 * 30485 * @-x-less Button.less 30486 * @class tinymce.ui.Button 30487 * @extends tinymce.ui.Widget 30488 */ 30489 define("tinymce/ui/Button", [ 30490 "tinymce/ui/Widget" 30491 ], function(Widget) { 30492 "use strict"; 30493 30494 return Widget.extend({ 30495 Defaults: { 30496 classes: "widget btn", 30497 role: "button" 30498 }, 30499 30500 /** 30501 * Constructs a new button instance with the specified settings. 30502 * 30503 * @constructor 30504 * @param {Object} settings Name/value object with settings. 30505 * @setting {String} size Size of the button small|medium|large. 30506 * @setting {String} image Image to use for icon. 30507 * @setting {String} icon Icon to use for button. 30508 */ 30509 init: function(settings) { 30510 var self = this, size; 30511 30512 self.on('click mousedown', function(e) { 30513 e.preventDefault(); 30514 }); 30515 30516 self._super(settings); 30517 size = settings.size; 30518 30519 if (settings.subtype) { 30520 self.addClass(settings.subtype); 30521 } 30522 30523 if (size) { 30524 self.addClass('btn-' + size); 30525 } 30526 }, 30527 30528 /** 30529 * Sets/gets the current button icon. 30530 * 30531 * @method icon 30532 * @param {String} [icon] New icon identifier. 30533 * @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance. 30534 */ 30535 icon: function(icon) { 30536 var self = this, prefix = self.classPrefix; 30537 30538 if (typeof(icon) == 'undefined') { 30539 return self.settings.icon; 30540 } 30541 30542 self.settings.icon = icon; 30543 icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 30544 30545 if (self._rendered) { 30546 var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0]; 30547 30548 if (icon) { 30549 if (!iconElm || iconElm != btnElm.firstChild) { 30550 iconElm = document.createElement('i'); 30551 btnElm.insertBefore(iconElm, btnElm.firstChild); 30552 } 30553 30554 iconElm.className = icon; 30555 } else if (iconElm) { 30556 btnElm.removeChild(iconElm); 30557 } 30558 30559 self.text(self._text); // Set text again to fix whitespace between icon + text 30560 } 30561 30562 return self; 30563 }, 30564 30565 /** 30566 * Repaints the button for example after it's been resizes by a layout engine. 30567 * 30568 * @method repaint 30569 */ 30570 repaint: function() { 30571 var btnStyle = this.getEl().firstChild.style; 30572 30573 btnStyle.width = btnStyle.height = "100%"; 30574 30575 this._super(); 30576 }, 30577 30578 /** 30579 * Sets/gets the current button text. 30580 * 30581 * @method text 30582 * @param {String} [text] New button text. 30583 * @return {String|tinymce.ui.Button} Current text or current Button instance. 30584 */ 30585 text: function(text) { 30586 var self = this; 30587 30588 if (self._rendered) { 30589 var textNode = self.getEl().lastChild.lastChild; 30590 if (textNode) { 30591 textNode.data = self.translate(text); 30592 } 30593 } 30594 30595 return self._super(text); 30596 }, 30597 30598 /** 30599 * Renders the control as a HTML string. 30600 * 30601 * @method renderHtml 30602 * @return {String} HTML representing the control. 30603 */ 30604 renderHtml: function() { 30605 var self = this, id = self._id, prefix = self.classPrefix; 30606 var icon = self.settings.icon, image; 30607 30608 image = self.settings.image; 30609 if (image) { 30610 icon = 'none'; 30611 30612 // Support for [high dpi, low dpi] image sources 30613 if (typeof image != "string") { 30614 image = window.getSelection ? image[0] : image[1]; 30615 } 30616 30617 image = ' style="background-image: url(\'' + image + '\')"'; 30618 } else { 30619 image = ''; 30620 } 30621 30622 icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; 30623 30624 return ( 30625 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' + 30626 '<button role="presentation" type="button" tabindex="-1">' + 30627 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 30628 (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + 30629 '</button>' + 30630 '</div>' 30631 ); 30632 } 30633 }); 30634 }); 30635 30636 // Included from: js/tinymce/classes/ui/ButtonGroup.js 30637 30638 /** 30639 * ButtonGroup.js 30640 * 30641 * Copyright, Moxiecode Systems AB 30642 * Released under LGPL License. 30643 * 30644 * License: http://www.tinymce.com/license 30645 * Contributing: http://www.tinymce.com/contributing 30646 */ 30647 30648 /** 30649 * This control enables you to put multiple buttons into a group. This is 30650 * useful when you want to combine similar toolbar buttons into a group. 30651 * 30652 * @example 30653 * // Create and render a buttongroup with two buttons to the body element 30654 * tinymce.ui.Factory.create({ 30655 * type: 'buttongroup', 30656 * items: [ 30657 * {text: 'Button A'}, 30658 * {text: 'Button B'} 30659 * ] 30660 * }).renderTo(document.body); 30661 * 30662 * @-x-less ButtonGroup.less 30663 * @class tinymce.ui.ButtonGroup 30664 * @extends tinymce.ui.Container 30665 */ 30666 define("tinymce/ui/ButtonGroup", [ 30667 "tinymce/ui/Container" 30668 ], function(Container) { 30669 "use strict"; 30670 30671 return Container.extend({ 30672 Defaults: { 30673 defaultType: 'button', 30674 role: 'group' 30675 }, 30676 30677 /** 30678 * Renders the control as a HTML string. 30679 * 30680 * @method renderHtml 30681 * @return {String} HTML representing the control. 30682 */ 30683 renderHtml: function() { 30684 var self = this, layout = self._layout; 30685 30686 self.addClass('btn-group'); 30687 self.preRender(); 30688 layout.preRender(self); 30689 30690 return ( 30691 '<div id="' + self._id + '" class="' + self.classes() + '">' + 30692 '<div id="' + self._id + '-body">' + 30693 (self.settings.html || '') + layout.renderHtml(self) + 30694 '</div>' + 30695 '</div>' 30696 ); 30697 } 30698 }); 30699 }); 30700 30701 // Included from: js/tinymce/classes/ui/Checkbox.js 30702 30703 /** 30704 * Checkbox.js 30705 * 30706 * Copyright, Moxiecode Systems AB 30707 * Released under LGPL License. 30708 * 30709 * License: http://www.tinymce.com/license 30710 * Contributing: http://www.tinymce.com/contributing 30711 */ 30712 30713 /** 30714 * This control creates a custom checkbox. 30715 * 30716 * @example 30717 * // Create and render a checkbox to the body element 30718 * tinymce.ui.Factory.create({ 30719 * type: 'checkbox', 30720 * checked: true, 30721 * text: 'My checkbox' 30722 * }).renderTo(document.body); 30723 * 30724 * @-x-less Checkbox.less 30725 * @class tinymce.ui.Checkbox 30726 * @extends tinymce.ui.Widget 30727 */ 30728 define("tinymce/ui/Checkbox", [ 30729 "tinymce/ui/Widget" 30730 ], function(Widget) { 30731 "use strict"; 30732 30733 return Widget.extend({ 30734 Defaults: { 30735 classes: "checkbox", 30736 role: "checkbox", 30737 checked: false 30738 }, 30739 30740 /** 30741 * Constructs a new Checkbox instance with the specified settings. 30742 * 30743 * @constructor 30744 * @param {Object} settings Name/value object with settings. 30745 * @setting {Boolean} checked True if the checkbox should be checked by default. 30746 */ 30747 init: function(settings) { 30748 var self = this; 30749 30750 self._super(settings); 30751 30752 self.on('click mousedown', function(e) { 30753 e.preventDefault(); 30754 }); 30755 30756 self.on('click', function(e) { 30757 e.preventDefault(); 30758 30759 if (!self.disabled()) { 30760 self.checked(!self.checked()); 30761 } 30762 }); 30763 30764 self.checked(self.settings.checked); 30765 }, 30766 30767 /** 30768 * Getter/setter function for the checked state. 30769 * 30770 * @method checked 30771 * @param {Boolean} [state] State to be set. 30772 * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. 30773 */ 30774 checked: function(state) { 30775 var self = this; 30776 30777 if (typeof state != "undefined") { 30778 if (state) { 30779 self.addClass('checked'); 30780 } else { 30781 self.removeClass('checked'); 30782 } 30783 30784 self._checked = state; 30785 self.aria('checked', state); 30786 30787 return self; 30788 } 30789 30790 return self._checked; 30791 }, 30792 30793 /** 30794 * Getter/setter function for the value state. 30795 * 30796 * @method value 30797 * @param {Boolean} [state] State to be set. 30798 * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. 30799 */ 30800 value: function(state) { 30801 return this.checked(state); 30802 }, 30803 30804 /** 30805 * Renders the control as a HTML string. 30806 * 30807 * @method renderHtml 30808 * @return {String} HTML representing the control. 30809 */ 30810 renderHtml: function() { 30811 var self = this, id = self._id, prefix = self.classPrefix; 30812 30813 return ( 30814 '<div id="' + id + '" class="' + self.classes() + '" unselectable="on" aria-labelledby="' + id + '-al" tabindex="-1">' + 30815 '<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' + 30816 '<span id="' + id + '-al" class="' + prefix + 'label">' + self.encode(self._text) + '</span>' + 30817 '</div>' 30818 ); 30819 } 30820 }); 30821 }); 30822 30823 // Included from: js/tinymce/classes/ui/ComboBox.js 30824 30825 /** 30826 * ComboBox.js 30827 * 30828 * Copyright, Moxiecode Systems AB 30829 * Released under LGPL License. 30830 * 30831 * License: http://www.tinymce.com/license 30832 * Contributing: http://www.tinymce.com/contributing 30833 */ 30834 30835 /** 30836 * This class creates a combobox control. Select box that you select a value from or 30837 * type a value into. 30838 * 30839 * @-x-less ComboBox.less 30840 * @class tinymce.ui.ComboBox 30841 * @extends tinymce.ui.Widget 30842 */ 30843 define("tinymce/ui/ComboBox", [ 30844 "tinymce/ui/Widget", 30845 "tinymce/ui/Factory", 30846 "tinymce/ui/DomUtils" 30847 ], function(Widget, Factory, DomUtils) { 30848 "use strict"; 30849 30850 return Widget.extend({ 30851 /** 30852 * Constructs a new control instance with the specified settings. 30853 * 30854 * @constructor 30855 * @param {Object} settings Name/value object with settings. 30856 * @setting {String} placeholder Placeholder text to display. 30857 */ 30858 init: function(settings) { 30859 var self = this; 30860 30861 self._super(settings); 30862 self.addClass('combobox'); 30863 self.subinput = true; 30864 self.ariaTarget = 'inp'; // TODO: Figure out a better way 30865 30866 settings = self.settings; 30867 settings.menu = settings.menu || settings.values; 30868 30869 if (settings.menu) { 30870 settings.icon = 'caret'; 30871 } 30872 30873 self.on('click', function(e) { 30874 var elm = e.target, root = self.getEl(); 30875 30876 while (elm && elm != root) { 30877 if (elm.id && elm.id.indexOf('-open') != -1) { 30878 self.fire('action'); 30879 30880 if (settings.menu) { 30881 self.showMenu(); 30882 30883 if (e.aria) { 30884 self.menu.items()[0].focus(); 30885 } 30886 } 30887 } 30888 30889 elm = elm.parentNode; 30890 } 30891 }); 30892 30893 // TODO: Rework this 30894 self.on('keydown', function(e) { 30895 if (e.target.nodeName == "INPUT" && e.keyCode == 13) { 30896 self.parents().reverse().each(function(ctrl) { 30897 e.preventDefault(); 30898 self.fire('change'); 30899 30900 if (ctrl.hasEventListeners('submit') && ctrl.toJSON) { 30901 ctrl.fire('submit', {data: ctrl.toJSON()}); 30902 return false; 30903 } 30904 }); 30905 } 30906 }); 30907 30908 if (settings.placeholder) { 30909 self.addClass('placeholder'); 30910 30911 self.on('focusin', function() { 30912 if (!self._hasOnChange) { 30913 DomUtils.on(self.getEl('inp'), 'change', function() { 30914 self.fire('change'); 30915 }); 30916 30917 self._hasOnChange = true; 30918 } 30919 30920 if (self.hasClass('placeholder')) { 30921 self.getEl('inp').value = ''; 30922 self.removeClass('placeholder'); 30923 } 30924 }); 30925 30926 self.on('focusout', function() { 30927 if (self.value().length === 0) { 30928 self.getEl('inp').value = settings.placeholder; 30929 self.addClass('placeholder'); 30930 } 30931 }); 30932 } 30933 }, 30934 30935 showMenu: function() { 30936 var self = this, settings = self.settings, menu; 30937 30938 if (!self.menu) { 30939 menu = settings.menu || []; 30940 30941 // Is menu array then auto constuct menu control 30942 if (menu.length) { 30943 menu = { 30944 type: 'menu', 30945 items: menu 30946 }; 30947 } else { 30948 menu.type = menu.type || 'menu'; 30949 } 30950 30951 self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm()); 30952 self.fire('createmenu'); 30953 self.menu.reflow(); 30954 self.menu.on('cancel', function(e) { 30955 if (e.control === self.menu) { 30956 self.focus(); 30957 } 30958 }); 30959 30960 self.menu.on('show hide', function(e) { 30961 e.control.items().each(function(ctrl) { 30962 ctrl.active(ctrl.value() == self.value()); 30963 }); 30964 }).fire('show'); 30965 30966 self.menu.on('select', function(e) { 30967 self.value(e.control.value()); 30968 }); 30969 30970 self.on('focusin', function(e) { 30971 if (e.target.tagName.toUpperCase() == 'INPUT') { 30972 self.menu.hide(); 30973 } 30974 }); 30975 30976 self.aria('expanded', true); 30977 } 30978 30979 self.menu.show(); 30980 self.menu.layoutRect({w: self.layoutRect().w}); 30981 self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); 30982 }, 30983 30984 /** 30985 * Getter/setter function for the control value. 30986 * 30987 * @method value 30988 * @param {String} [value] Value to be set. 30989 * @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation. 30990 */ 30991 value: function(value) { 30992 var self = this; 30993 30994 if (typeof(value) != "undefined") { 30995 self._value = value; 30996 self.removeClass('placeholder'); 30997 30998 if (self._rendered) { 30999 self.getEl('inp').value = value; 31000 } 31001 31002 return self; 31003 } 31004 31005 if (self._rendered) { 31006 value = self.getEl('inp').value; 31007 31008 if (value != self.settings.placeholder) { 31009 return value; 31010 } 31011 31012 return ''; 31013 } 31014 31015 return self._value; 31016 }, 31017 31018 /** 31019 * Getter/setter function for the disabled state. 31020 * 31021 * @method value 31022 * @param {Boolean} [state] State to be set. 31023 * @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation. 31024 */ 31025 disabled: function(state) { 31026 var self = this; 31027 31028 if (self._rendered && typeof(state) != 'undefined') { 31029 self.getEl('inp').disabled = state; 31030 } 31031 31032 return self._super(state); 31033 }, 31034 31035 /** 31036 * Focuses the input area of the control. 31037 * 31038 * @method focus 31039 */ 31040 focus: function() { 31041 this.getEl('inp').focus(); 31042 }, 31043 31044 /** 31045 * Repaints the control after a layout operation. 31046 * 31047 * @method repaint 31048 */ 31049 repaint: function() { 31050 var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect(); 31051 var width, lineHeight; 31052 31053 if (openElm) { 31054 width = rect.w - DomUtils.getSize(openElm).width - 10; 31055 } else { 31056 width = rect.w - 10; 31057 } 31058 31059 // Detect old IE 7+8 add lineHeight to align caret vertically in the middle 31060 var doc = document; 31061 if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) { 31062 lineHeight = (self.layoutRect().h - 2) + 'px'; 31063 } 31064 31065 DomUtils.css(elm.firstChild, { 31066 width: width, 31067 lineHeight: lineHeight 31068 }); 31069 31070 self._super(); 31071 31072 return self; 31073 }, 31074 31075 /** 31076 * Post render method. Called after the control has been rendered to the target. 31077 * 31078 * @method postRender 31079 * @return {tinymce.ui.ComboBox} Current combobox instance. 31080 */ 31081 postRender: function() { 31082 var self = this; 31083 31084 DomUtils.on(this.getEl('inp'), 'change', function() { 31085 self.fire('change'); 31086 }); 31087 31088 return self._super(); 31089 }, 31090 31091 remove: function() { 31092 DomUtils.off(this.getEl('inp')); 31093 this._super(); 31094 }, 31095 31096 /** 31097 * Renders the control as a HTML string. 31098 * 31099 * @method renderHtml 31100 * @return {String} HTML representing the control. 31101 */ 31102 renderHtml: function() { 31103 var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix; 31104 var value = settings.value || settings.placeholder || ''; 31105 var icon, text, openBtnHtml = '', extraAttrs = ''; 31106 31107 if ("spellcheck" in settings) { 31108 extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; 31109 } 31110 31111 if (settings.maxLength) { 31112 extraAttrs += ' maxlength="' + settings.maxLength + '"'; 31113 } 31114 31115 if (settings.size) { 31116 extraAttrs += ' size="' + settings.size + '"'; 31117 } 31118 31119 if (settings.subtype) { 31120 extraAttrs += ' type="' + settings.subtype + '"'; 31121 } 31122 31123 if (self.disabled()) { 31124 extraAttrs += ' disabled="disabled"'; 31125 } 31126 31127 icon = settings.icon; 31128 if (icon && icon != 'caret') { 31129 icon = prefix + 'ico ' + prefix + 'i-' + settings.icon; 31130 } 31131 31132 text = self._text; 31133 31134 if (icon || text) { 31135 openBtnHtml = ( 31136 '<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' + 31137 '<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' + 31138 (icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') + 31139 (text ? (icon ? ' ' : '') + text : '') + 31140 '</button>' + 31141 '</div>' 31142 ); 31143 31144 self.addClass('has-open'); 31145 } 31146 31147 return ( 31148 '<div id="' + id + '" class="' + self.classes() + '">' + 31149 '<input id="' + id + '-inp" class="' + prefix + 'textbox ' + prefix + 'placeholder" value="' + 31150 value + '" hidefocus="1"' + extraAttrs + ' />' + 31151 openBtnHtml + 31152 '</div>' 31153 ); 31154 } 31155 }); 31156 }); 31157 31158 // Included from: js/tinymce/classes/ui/ColorBox.js 31159 31160 /** 31161 * ColorBox.js 31162 * 31163 * Copyright, Moxiecode Systems AB 31164 * Released under LGPL License. 31165 * 31166 * License: http://www.tinymce.com/license 31167 * Contributing: http://www.tinymce.com/contributing 31168 */ 31169 31170 /** 31171 * This widget lets you enter colors and browse for colors by pressing the color button. It also displays 31172 * a preview of the current color. 31173 * 31174 * @-x-less ColorBox.less 31175 * @class tinymce.ui.ColorBox 31176 * @extends tinymce.ui.ComboBox 31177 */ 31178 define("tinymce/ui/ColorBox", [ 31179 "tinymce/ui/ComboBox" 31180 ], function(ComboBox) { 31181 "use strict"; 31182 31183 return ComboBox.extend({ 31184 /** 31185 * Constructs a new control instance with the specified settings. 31186 * 31187 * @constructor 31188 * @param {Object} settings Name/value object with settings. 31189 */ 31190 init: function(settings) { 31191 var self = this; 31192 31193 settings.spellcheck = false; 31194 31195 if (settings.onaction) { 31196 settings.icon = 'none'; 31197 } 31198 31199 self._super(settings); 31200 31201 self.addClass('colorbox'); 31202 self.on('change keyup postrender', function() { 31203 self.repaintColor(self.value()); 31204 }); 31205 }, 31206 31207 repaintColor: function(value) { 31208 var elm = this.getEl().getElementsByTagName('i')[0]; 31209 31210 if (elm) { 31211 try { 31212 elm.style.background = value; 31213 } catch (ex) { 31214 // Ignore 31215 } 31216 } 31217 }, 31218 31219 value: function(value) { 31220 var self = this; 31221 31222 if (typeof value != "undefined") { 31223 if (self._rendered) { 31224 self.repaintColor(value); 31225 } 31226 } 31227 31228 return self._super(value); 31229 } 31230 }); 31231 }); 31232 31233 // Included from: js/tinymce/classes/ui/PanelButton.js 31234 31235 /** 31236 * PanelButton.js 31237 * 31238 * Copyright, Moxiecode Systems AB 31239 * Released under LGPL License. 31240 * 31241 * License: http://www.tinymce.com/license 31242 * Contributing: http://www.tinymce.com/contributing 31243 */ 31244 31245 /** 31246 * Creates a new panel button. 31247 * 31248 * @class tinymce.ui.PanelButton 31249 * @extends tinymce.ui.Button 31250 */ 31251 define("tinymce/ui/PanelButton", [ 31252 "tinymce/ui/Button", 31253 "tinymce/ui/FloatPanel" 31254 ], function(Button, FloatPanel) { 31255 "use strict"; 31256 31257 return Button.extend({ 31258 /** 31259 * Shows the panel for the button. 31260 * 31261 * @method showPanel 31262 */ 31263 showPanel: function() { 31264 var self = this, settings = self.settings; 31265 31266 self.active(true); 31267 31268 if (!self.panel) { 31269 var panelSettings = settings.panel; 31270 31271 // Wrap panel in grid layout if type if specified 31272 // This makes it possible to add forms or other containers directly in the panel option 31273 if (panelSettings.type) { 31274 panelSettings = { 31275 layout: 'grid', 31276 items: panelSettings 31277 }; 31278 } 31279 31280 panelSettings.role = panelSettings.role || 'dialog'; 31281 panelSettings.popover = true; 31282 panelSettings.autohide = true; 31283 panelSettings.ariaRoot = true; 31284 31285 self.panel = new FloatPanel(panelSettings).on('hide', function() { 31286 self.active(false); 31287 }).on('cancel', function(e) { 31288 e.stopPropagation(); 31289 self.focus(); 31290 self.hidePanel(); 31291 }).parent(self).renderTo(self.getContainerElm()); 31292 31293 self.panel.fire('show'); 31294 self.panel.reflow(); 31295 } else { 31296 self.panel.show(); 31297 } 31298 31299 self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc'])); 31300 }, 31301 31302 /** 31303 * Hides the panel for the button. 31304 * 31305 * @method hidePanel 31306 */ 31307 hidePanel: function() { 31308 var self = this; 31309 31310 if (self.panel) { 31311 self.panel.hide(); 31312 } 31313 }, 31314 31315 /** 31316 * Called after the control has been rendered. 31317 * 31318 * @method postRender 31319 */ 31320 postRender: function() { 31321 var self = this; 31322 31323 self.aria('haspopup', true); 31324 31325 self.on('click', function(e) { 31326 if (e.control === self) { 31327 if (self.panel && self.panel.visible()) { 31328 self.hidePanel(); 31329 } else { 31330 self.showPanel(); 31331 self.panel.focus(!!e.aria); 31332 } 31333 } 31334 }); 31335 31336 return self._super(); 31337 }, 31338 31339 remove: function() { 31340 if (this.panel) { 31341 this.panel.remove(); 31342 this.panel = null; 31343 } 31344 31345 return this._super(); 31346 } 31347 }); 31348 }); 31349 31350 // Included from: js/tinymce/classes/ui/ColorButton.js 31351 31352 /** 31353 * ColorButton.js 31354 * 31355 * Copyright, Moxiecode Systems AB 31356 * Released under LGPL License. 31357 * 31358 * License: http://www.tinymce.com/license 31359 * Contributing: http://www.tinymce.com/contributing 31360 */ 31361 31362 /** 31363 * This class creates a color button control. This is a split button in which the main 31364 * button has a visual representation of the currently selected color. When clicked 31365 * the caret button displays a color picker, allowing the user to select a new color. 31366 * 31367 * @-x-less ColorButton.less 31368 * @class tinymce.ui.ColorButton 31369 * @extends tinymce.ui.PanelButton 31370 */ 31371 define("tinymce/ui/ColorButton", [ 31372 "tinymce/ui/PanelButton", 31373 "tinymce/dom/DOMUtils" 31374 ], function(PanelButton, DomUtils) { 31375 "use strict"; 31376 31377 var DOM = DomUtils.DOM; 31378 31379 return PanelButton.extend({ 31380 /** 31381 * Constructs a new ColorButton instance with the specified settings. 31382 * 31383 * @constructor 31384 * @param {Object} settings Name/value object with settings. 31385 */ 31386 init: function(settings) { 31387 this._super(settings); 31388 this.addClass('colorbutton'); 31389 }, 31390 31391 /** 31392 * Getter/setter for the current color. 31393 * 31394 * @method color 31395 * @param {String} [color] Color to set. 31396 * @return {String|tinymce.ui.ColorButton} Current color or current instance. 31397 */ 31398 color: function(color) { 31399 if (color) { 31400 this._color = color; 31401 this.getEl('preview').style.backgroundColor = color; 31402 return this; 31403 } 31404 31405 return this._color; 31406 }, 31407 31408 /** 31409 * Renders the control as a HTML string. 31410 * 31411 * @method renderHtml 31412 * @return {String} HTML representing the control. 31413 */ 31414 renderHtml: function() { 31415 var self = this, id = self._id, prefix = self.classPrefix; 31416 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 31417 var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : ''; 31418 31419 return ( 31420 '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1" aria-haspopup="true">' + 31421 '<button role="presentation" hidefocus="1" type="button" tabindex="-1">' + 31422 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 31423 '<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' + 31424 (self._text ? (icon ? ' ' : '') + (self._text) : '') + 31425 '</button>' + 31426 '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + 31427 ' <i class="' + prefix + 'caret"></i>' + 31428 '</button>' + 31429 '</div>' 31430 ); 31431 }, 31432 31433 /** 31434 * Called after the control has been rendered. 31435 * 31436 * @method postRender 31437 */ 31438 postRender: function() { 31439 var self = this, onClickHandler = self.settings.onclick; 31440 31441 self.on('click', function(e) { 31442 if (e.aria && e.aria.key == 'down') { 31443 return; 31444 } 31445 31446 if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) { 31447 e.stopImmediatePropagation(); 31448 onClickHandler.call(self, e); 31449 } 31450 }); 31451 31452 delete self.settings.onclick; 31453 31454 return self._super(); 31455 } 31456 }); 31457 }); 31458 31459 // Included from: js/tinymce/classes/util/Color.js 31460 31461 /** 31462 * Color.js 31463 * 31464 * Copyright, Moxiecode Systems AB 31465 * Released under LGPL License. 31466 * 31467 * License: http://www.tinymce.com/license 31468 * Contributing: http://www.tinymce.com/contributing 31469 */ 31470 31471 /** 31472 * This class lets you parse/serialize colors and convert rgb/hsb. 31473 * 31474 * @class tinymce.util.Color 31475 * @example 31476 * var white = new tinymce.util.Color({r: 255, g: 255, b: 255}); 31477 * var red = new tinymce.util.Color('#FF0000'); 31478 * 31479 * console.log(white.toHex(), red.toHsv()); 31480 */ 31481 define("tinymce/util/Color", [], function() { 31482 var min = Math.min, max = Math.max, round = Math.round; 31483 31484 /** 31485 * Constructs a new color instance. 31486 * 31487 * @constructor 31488 * @method Color 31489 * @param {String} value Optional initial value to parse. 31490 */ 31491 function Color(value) { 31492 var self = this, r = 0, g = 0, b = 0; 31493 31494 function rgb2hsv(r, g, b) { 31495 var h, s, v, d, minRGB, maxRGB; 31496 31497 h = 0; 31498 s = 0; 31499 v = 0; 31500 r = r / 255; 31501 g = g / 255; 31502 b = b / 255; 31503 31504 minRGB = min(r, min(g, b)); 31505 maxRGB = max(r, max(g, b)); 31506 31507 if (minRGB == maxRGB) { 31508 v = minRGB; 31509 31510 return { 31511 h: 0, 31512 s: 0, 31513 v: v * 100 31514 }; 31515 } 31516 31517 /*eslint no-nested-ternary:0 */ 31518 d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r); 31519 h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5); 31520 h = 60 * (h - d / (maxRGB - minRGB)); 31521 s = (maxRGB - minRGB) / maxRGB; 31522 v = maxRGB; 31523 31524 return { 31525 h: round(h), 31526 s: round(s * 100), 31527 v: round(v * 100) 31528 }; 31529 } 31530 31531 function hsvToRgb(hue, saturation, brightness) { 31532 var side, chroma, x, match; 31533 31534 hue = (parseInt(hue, 10) || 0) % 360; 31535 saturation = parseInt(saturation, 10) / 100; 31536 brightness = parseInt(brightness, 10) / 100; 31537 saturation = max(0, min(saturation, 1)); 31538 brightness = max(0, min(brightness, 1)); 31539 31540 if (saturation === 0) { 31541 r = g = b = round(255 * brightness); 31542 return; 31543 } 31544 31545 side = hue / 60; 31546 chroma = brightness * saturation; 31547 x = chroma * (1 - Math.abs(side % 2 - 1)); 31548 match = brightness - chroma; 31549 31550 switch (Math.floor(side)) { 31551 case 0: 31552 r = chroma; 31553 g = x; 31554 b = 0; 31555 break; 31556 31557 case 1: 31558 r = x; 31559 g = chroma; 31560 b = 0; 31561 break; 31562 31563 case 2: 31564 r = 0; 31565 g = chroma; 31566 b = x; 31567 break; 31568 31569 case 3: 31570 r = 0; 31571 g = x; 31572 b = chroma; 31573 break; 31574 31575 case 4: 31576 r = x; 31577 g = 0; 31578 b = chroma; 31579 break; 31580 31581 case 5: 31582 r = chroma; 31583 g = 0; 31584 b = x; 31585 break; 31586 31587 default: 31588 r = g = b = 0; 31589 } 31590 31591 r = round(255 * (r + match)); 31592 g = round(255 * (g + match)); 31593 b = round(255 * (b + match)); 31594 } 31595 31596 /** 31597 * Returns the hex string of the current color. For example: #ff00ff 31598 * 31599 * @method toHex 31600 * @return {String} Hex string of current color. 31601 */ 31602 function toHex() { 31603 function hex(val) { 31604 val = parseInt(val, 10).toString(16); 31605 31606 return val.length > 1 ? val : '0' + val; 31607 } 31608 31609 return '#' + hex(r) + hex(g) + hex(b); 31610 } 31611 31612 /** 31613 * Returns the r, g, b values of the color. Each channel has a range from 0-255. 31614 * 31615 * @method toRgb 31616 * @return {Object} Object with r, g, b fields. 31617 */ 31618 function toRgb() { 31619 return { 31620 r: r, 31621 g: g, 31622 b: b 31623 }; 31624 } 31625 31626 /** 31627 * Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100. 31628 * 31629 * @method toHsv 31630 * @return {Object} Object with h, s, v fields. 31631 */ 31632 function toHsv() { 31633 return rgb2hsv(r, g, b); 31634 } 31635 31636 /** 31637 * Parses the specified value and populates the color instance. 31638 * 31639 * Supported format examples: 31640 * * rbg(255,0,0) 31641 * * #ff0000 31642 * * #fff 31643 * * {r: 255, g: 0, b: 0} 31644 * * {h: 360, s: 100, v: 100} 31645 * 31646 * @method parse 31647 * @param {Object/String} value Color value to parse. 31648 * @return {tinymce.util.Color} Current color instance. 31649 */ 31650 function parse(value) { 31651 var matches; 31652 31653 if (typeof value == 'object') { 31654 if ("r" in value) { 31655 r = value.r; 31656 g = value.g; 31657 b = value.b; 31658 } else if ("v" in value) { 31659 hsvToRgb(value.h, value.s, value.v); 31660 } 31661 } else { 31662 if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) { 31663 r = parseInt(matches[1], 10); 31664 g = parseInt(matches[2], 10); 31665 b = parseInt(matches[3], 10); 31666 } else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) { 31667 r = parseInt(matches[1], 16); 31668 g = parseInt(matches[2], 16); 31669 b = parseInt(matches[3], 16); 31670 } else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) { 31671 r = parseInt(matches[1] + matches[1], 16); 31672 g = parseInt(matches[2] + matches[2], 16); 31673 b = parseInt(matches[3] + matches[3], 16); 31674 } 31675 } 31676 31677 r = r < 0 ? 0 : (r > 255 ? 255 : r); 31678 g = g < 0 ? 0 : (g > 255 ? 255 : g); 31679 b = b < 0 ? 0 : (b > 255 ? 255 : b); 31680 31681 return self; 31682 } 31683 31684 if (value) { 31685 parse(value); 31686 } 31687 31688 self.toRgb = toRgb; 31689 self.toHsv = toHsv; 31690 self.toHex = toHex; 31691 self.parse = parse; 31692 } 31693 31694 return Color; 31695 }); 31696 31697 // Included from: js/tinymce/classes/ui/ColorPicker.js 31698 31699 /** 31700 * ColorPicker.js 31701 * 31702 * Copyright, Moxiecode Systems AB 31703 * Released under LGPL License. 31704 * 31705 * License: http://www.tinymce.com/license 31706 * Contributing: http://www.tinymce.com/contributing 31707 */ 31708 31709 /** 31710 * Color picker widget lets you select colors. 31711 * 31712 * @-x-less ColorPicker.less 31713 * @class tinymce.ui.ColorPicker 31714 * @extends tinymce.ui.Widget 31715 */ 31716 define("tinymce/ui/ColorPicker", [ 31717 "tinymce/ui/Widget", 31718 "tinymce/ui/DragHelper", 31719 "tinymce/ui/DomUtils", 31720 "tinymce/util/Color" 31721 ], function(Widget, DragHelper, DomUtils, Color) { 31722 "use strict"; 31723 31724 return Widget.extend({ 31725 Defaults: { 31726 classes: "widget colorpicker" 31727 }, 31728 31729 /** 31730 * Constructs a new colorpicker instance with the specified settings. 31731 * 31732 * @constructor 31733 * @param {Object} settings Name/value object with settings. 31734 * @setting {String} color Initial color value. 31735 */ 31736 init: function(settings) { 31737 this._super(settings); 31738 }, 31739 31740 postRender: function() { 31741 var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm; 31742 31743 hueRootElm = self.getEl('h'); 31744 huePointElm = self.getEl('hp'); 31745 svRootElm = self.getEl('sv'); 31746 svPointElm = self.getEl('svp'); 31747 31748 function getPos(elm, event) { 31749 var pos = DomUtils.getPos(elm), x, y; 31750 31751 x = event.pageX - pos.x; 31752 y = event.pageY - pos.y; 31753 31754 x = Math.max(0, Math.min(x / elm.clientWidth, 1)); 31755 y = Math.max(0, Math.min(y / elm.clientHeight, 1)); 31756 31757 return { 31758 x: x, 31759 y: y 31760 }; 31761 } 31762 31763 function updateColor(hsv, hueUpdate) { 31764 var hue = (360 - hsv.h) / 360; 31765 31766 DomUtils.css(huePointElm, { 31767 top: (hue * 100) + '%' 31768 }); 31769 31770 if (!hueUpdate) { 31771 DomUtils.css(svPointElm, { 31772 left: hsv.s + '%', 31773 top: (100 - hsv.v) + '%' 31774 }); 31775 } 31776 31777 svRootElm.style.background = new Color({s: 100, v: 100, h: hsv.h}).toHex(); 31778 self.color().parse({s: hsv.s, v: hsv.v, h: hsv.h}); 31779 } 31780 31781 function updateSaturationAndValue(e) { 31782 var pos; 31783 31784 pos = getPos(svRootElm, e); 31785 hsv.s = pos.x * 100; 31786 hsv.v = (1 - pos.y) * 100; 31787 31788 updateColor(hsv); 31789 self.fire('change'); 31790 } 31791 31792 function updateHue(e) { 31793 var pos; 31794 31795 pos = getPos(hueRootElm, e); 31796 hsv = color.toHsv(); 31797 hsv.h = (1 - pos.y) * 360; 31798 updateColor(hsv, true); 31799 self.fire('change'); 31800 } 31801 31802 self._repaint = function() { 31803 hsv = color.toHsv(); 31804 updateColor(hsv); 31805 }; 31806 31807 self._super(); 31808 31809 self._svdraghelper = new DragHelper(self._id + '-sv', { 31810 start: updateSaturationAndValue, 31811 drag: updateSaturationAndValue 31812 }); 31813 31814 self._hdraghelper = new DragHelper(self._id + '-h', { 31815 start: updateHue, 31816 drag: updateHue 31817 }); 31818 31819 self._repaint(); 31820 }, 31821 31822 rgb: function() { 31823 return this.color().toRgb(); 31824 }, 31825 31826 value: function(value) { 31827 var self = this; 31828 31829 if (arguments.length) { 31830 self.color().parse(value); 31831 31832 if (self._rendered) { 31833 self._repaint(); 31834 } 31835 } else { 31836 return self.color().toHex(); 31837 } 31838 }, 31839 31840 color: function() { 31841 if (!this._color) { 31842 this._color = new Color(); 31843 } 31844 31845 return this._color; 31846 }, 31847 31848 /** 31849 * Renders the control as a HTML string. 31850 * 31851 * @method renderHtml 31852 * @return {String} HTML representing the control. 31853 */ 31854 renderHtml: function() { 31855 var self = this, id = self._id, prefix = self.classPrefix, hueHtml; 31856 var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000'; 31857 31858 function getOldIeFallbackHtml() { 31859 var i, l, html = '', gradientPrefix, stopsList; 31860 31861 gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='; 31862 stopsList = stops.split(','); 31863 for (i = 0, l = stopsList.length - 1; i < l; i++) { 31864 html += ( 31865 '<div class="' + prefix + 'colorpicker-h-chunk" style="' + 31866 'height:' + (100 / l) + '%;' + 31867 gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ');' + 31868 '-ms-' + gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ')' + 31869 '"></div>' 31870 ); 31871 } 31872 31873 return html; 31874 } 31875 31876 var gradientCssText = ( 31877 'background: -ms-linear-gradient(top,' + stops + ');' + 31878 'background: linear-gradient(to bottom,' + stops + ');' 31879 ); 31880 31881 hueHtml = ( 31882 '<div id="' + id + '-h" class="' + prefix + 'colorpicker-h" style="' + gradientCssText + '">' + 31883 getOldIeFallbackHtml() + 31884 '<div id="' + id + '-hp" class="' + prefix + 'colorpicker-h-marker"></div>' + 31885 '</div>' 31886 ); 31887 31888 return ( 31889 '<div id="' + id + '" class="' + self.classes() + '">' + 31890 '<div id="' + id + '-sv" class="' + prefix + 'colorpicker-sv">' + 31891 '<div class="' + prefix + 'colorpicker-overlay1">' + 31892 '<div class="' + prefix + 'colorpicker-overlay2">' + 31893 '<div id="' + id + '-svp" class="' + prefix + 'colorpicker-selector1">' + 31894 '<div class="' + prefix + 'colorpicker-selector2"></div>' + 31895 '</div>' + 31896 '</div>' + 31897 '</div>' + 31898 '</div>' + 31899 hueHtml + 31900 '</div>' 31901 ); 31902 } 31903 }); 31904 }); 31905 31906 // Included from: js/tinymce/classes/ui/Path.js 31907 31908 /** 31909 * Path.js 31910 * 31911 * Copyright, Moxiecode Systems AB 31912 * Released under LGPL License. 31913 * 31914 * License: http://www.tinymce.com/license 31915 * Contributing: http://www.tinymce.com/contributing 31916 */ 31917 31918 /** 31919 * Creates a new path control. 31920 * 31921 * @-x-less Path.less 31922 * @class tinymce.ui.Path 31923 * @extends tinymce.ui.Widget 31924 */ 31925 define("tinymce/ui/Path", [ 31926 "tinymce/ui/Widget" 31927 ], function(Widget) { 31928 "use strict"; 31929 31930 return Widget.extend({ 31931 /** 31932 * Constructs a instance with the specified settings. 31933 * 31934 * @constructor 31935 * @param {Object} settings Name/value object with settings. 31936 * @setting {String} delimiter Delimiter to display between items in path. 31937 */ 31938 init: function(settings) { 31939 var self = this; 31940 31941 if (!settings.delimiter) { 31942 settings.delimiter = '\u00BB'; 31943 } 31944 31945 self._super(settings); 31946 self.addClass('path'); 31947 self.canFocus = true; 31948 31949 self.on('click', function(e) { 31950 var index, target = e.target; 31951 31952 if ((index = target.getAttribute('data-index'))) { 31953 self.fire('select', {value: self.data()[index], index: index}); 31954 } 31955 }); 31956 }, 31957 31958 /** 31959 * Focuses the current control. 31960 * 31961 * @method focus 31962 * @return {tinymce.ui.Control} Current control instance. 31963 */ 31964 focus: function() { 31965 var self = this; 31966 31967 self.getEl().firstChild.focus(); 31968 31969 return self; 31970 }, 31971 31972 /** 31973 * Sets/gets the data to be used for the path. 31974 * 31975 * @method data 31976 * @param {Array} data Array with items name is rendered to path. 31977 */ 31978 data: function(data) { 31979 var self = this; 31980 31981 if (typeof(data) !== "undefined") { 31982 self._data = data; 31983 self.update(); 31984 31985 return self; 31986 } 31987 31988 return self._data; 31989 }, 31990 31991 /** 31992 * Updated the path. 31993 * 31994 * @private 31995 */ 31996 update: function() { 31997 this.innerHtml(this._getPathHtml()); 31998 }, 31999 32000 /** 32001 * Called after the control has been rendered. 32002 * 32003 * @method postRender 32004 */ 32005 postRender: function() { 32006 var self = this; 32007 32008 self._super(); 32009 32010 self.data(self.settings.data); 32011 }, 32012 32013 /** 32014 * Renders the control as a HTML string. 32015 * 32016 * @method renderHtml 32017 * @return {String} HTML representing the control. 32018 */ 32019 renderHtml: function() { 32020 var self = this; 32021 32022 return ( 32023 '<div id="' + self._id + '" class="' + self.classes() + '">' + 32024 self._getPathHtml() + 32025 '</div>' 32026 ); 32027 }, 32028 32029 _getPathHtml: function() { 32030 var self = this, parts = self._data || [], i, l, html = '', prefix = self.classPrefix; 32031 32032 for (i = 0, l = parts.length; i < l; i++) { 32033 html += ( 32034 (i > 0 ? '<div class="' + prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') + 32035 '<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' + 32036 i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + i + '">' + parts[i].name + '</div>' 32037 ); 32038 } 32039 32040 if (!html) { 32041 html = '<div class="' + prefix + 'path-item">\u00a0</div>'; 32042 } 32043 32044 return html; 32045 } 32046 }); 32047 }); 32048 32049 // Included from: js/tinymce/classes/ui/ElementPath.js 32050 32051 /** 32052 * ElementPath.js 32053 * 32054 * Copyright, Moxiecode Systems AB 32055 * Released under LGPL License. 32056 * 32057 * License: http://www.tinymce.com/license 32058 * Contributing: http://www.tinymce.com/contributing 32059 */ 32060 32061 /** 32062 * This control creates an path for the current selections parent elements in TinyMCE. 32063 * 32064 * @class tinymce.ui.ElementPath 32065 * @extends tinymce.ui.Path 32066 */ 32067 define("tinymce/ui/ElementPath", [ 32068 "tinymce/ui/Path", 32069 "tinymce/EditorManager" 32070 ], function(Path, EditorManager) { 32071 return Path.extend({ 32072 /** 32073 * Post render method. Called after the control has been rendered to the target. 32074 * 32075 * @method postRender 32076 * @return {tinymce.ui.ElementPath} Current combobox instance. 32077 */ 32078 postRender: function() { 32079 var self = this, editor = EditorManager.activeEditor; 32080 32081 function isHidden(elm) { 32082 if (elm.nodeType === 1) { 32083 if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) { 32084 return true; 32085 } 32086 32087 if (elm.getAttribute('data-mce-type') === 'bookmark') { 32088 return true; 32089 } 32090 } 32091 32092 return false; 32093 } 32094 32095 self.on('select', function(e) { 32096 editor.focus(); 32097 editor.selection.select(this.data()[e.index].element); 32098 editor.nodeChanged(); 32099 }); 32100 32101 editor.on('nodeChange', function(e) { 32102 var outParents = [], parents = e.parents, i = parents.length; 32103 32104 while (i--) { 32105 if (parents[i].nodeType == 1 && !isHidden(parents[i])) { 32106 var args = editor.fire('ResolveName', { 32107 name: parents[i].nodeName.toLowerCase(), 32108 target: parents[i] 32109 }); 32110 32111 if (!args.isDefaultPrevented()) { 32112 outParents.push({name: args.name, element: parents[i]}); 32113 } 32114 32115 if (args.isPropagationStopped()) { 32116 break; 32117 } 32118 } 32119 } 32120 32121 self.data(outParents); 32122 }); 32123 32124 return self._super(); 32125 } 32126 }); 32127 }); 32128 32129 // Included from: js/tinymce/classes/ui/FormItem.js 32130 32131 /** 32132 * FormItem.js 32133 * 32134 * Copyright, Moxiecode Systems AB 32135 * Released under LGPL License. 32136 * 32137 * License: http://www.tinymce.com/license 32138 * Contributing: http://www.tinymce.com/contributing 32139 */ 32140 32141 /** 32142 * This class is a container created by the form element with 32143 * a label and control item. 32144 * 32145 * @class tinymce.ui.FormItem 32146 * @extends tinymce.ui.Container 32147 * @setting {String} label Label to display for the form item. 32148 */ 32149 define("tinymce/ui/FormItem", [ 32150 "tinymce/ui/Container" 32151 ], function(Container) { 32152 "use strict"; 32153 32154 return Container.extend({ 32155 Defaults: { 32156 layout: 'flex', 32157 align: 'center', 32158 defaults: { 32159 flex: 1 32160 } 32161 }, 32162 32163 /** 32164 * Renders the control as a HTML string. 32165 * 32166 * @method renderHtml 32167 * @return {String} HTML representing the control. 32168 */ 32169 renderHtml: function() { 32170 var self = this, layout = self._layout, prefix = self.classPrefix; 32171 32172 self.addClass('formitem'); 32173 layout.preRender(self); 32174 32175 return ( 32176 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 32177 (self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' + 32178 self.settings.title + '</div>') : '') + 32179 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 32180 (self.settings.html || '') + layout.renderHtml(self) + 32181 '</div>' + 32182 '</div>' 32183 ); 32184 } 32185 }); 32186 }); 32187 32188 // Included from: js/tinymce/classes/ui/Form.js 32189 32190 /** 32191 * Form.js 32192 * 32193 * Copyright, Moxiecode Systems AB 32194 * Released under LGPL License. 32195 * 32196 * License: http://www.tinymce.com/license 32197 * Contributing: http://www.tinymce.com/contributing 32198 */ 32199 32200 /** 32201 * This class creates a form container. A form container has the ability 32202 * to automatically wrap items in tinymce.ui.FormItem instances. 32203 * 32204 * Each FormItem instance is a container for the label and the item. 32205 * 32206 * @example 32207 * tinymce.ui.Factory.create({ 32208 * type: 'form', 32209 * items: [ 32210 * {type: 'textbox', label: 'My text box'} 32211 * ] 32212 * }).renderTo(document.body); 32213 * 32214 * @class tinymce.ui.Form 32215 * @extends tinymce.ui.Container 32216 */ 32217 define("tinymce/ui/Form", [ 32218 "tinymce/ui/Container", 32219 "tinymce/ui/FormItem", 32220 "tinymce/util/Tools" 32221 ], function(Container, FormItem, Tools) { 32222 "use strict"; 32223 32224 return Container.extend({ 32225 Defaults: { 32226 containerCls: 'form', 32227 layout: 'flex', 32228 direction: 'column', 32229 align: 'stretch', 32230 flex: 1, 32231 padding: 20, 32232 labelGap: 30, 32233 spacing: 10, 32234 callbacks: { 32235 submit: function() { 32236 this.submit(); 32237 } 32238 } 32239 }, 32240 32241 /** 32242 * This method gets invoked before the control is rendered. 32243 * 32244 * @method preRender 32245 */ 32246 preRender: function() { 32247 var self = this, items = self.items(); 32248 32249 if (!self.settings.formItemDefaults) { 32250 self.settings.formItemDefaults = { 32251 layout: 'flex', 32252 autoResize: "overflow", 32253 defaults: {flex: 1} 32254 }; 32255 } 32256 32257 // Wrap any labeled items in FormItems 32258 items.each(function(ctrl) { 32259 var formItem, inputId, label = ctrl.settings.label; 32260 32261 if (label) { 32262 inputId = ctrl._id; 32263 32264 // point to the INPUTs of comboxes 32265 // see the corresbonding TODO in ComboBox.js 32266 if (ctrl.subinput) { 32267 inputId += '-' + ctrl.ariaTarget; 32268 } 32269 32270 formItem = new FormItem(Tools.extend({ 32271 items: { 32272 type: 'label', 32273 id: ctrl._id + '-l', 32274 text: label, 32275 flex: 0, 32276 forId: inputId, 32277 disabled: ctrl.disabled() 32278 } 32279 }, self.settings.formItemDefaults)); 32280 32281 formItem.type = 'formitem'; 32282 ctrl.aria('labelledby', ctrl._id + '-l'); 32283 32284 if (typeof(ctrl.settings.flex) == "undefined") { 32285 ctrl.settings.flex = 1; 32286 } 32287 32288 self.replace(ctrl, formItem); 32289 formItem.add(ctrl); 32290 } 32291 }); 32292 }, 32293 32294 /** 32295 * Recalcs label widths. 32296 * 32297 * @private 32298 */ 32299 recalcLabels: function() { 32300 var self = this, maxLabelWidth = 0, labels = [], i, labelGap, items; 32301 32302 if (self.settings.labelGapCalc === false) { 32303 return; 32304 } 32305 32306 if (self.settings.labelGapCalc == "children") { 32307 items = self.find('formitem'); 32308 } else { 32309 items = self.items(); 32310 } 32311 32312 items.filter('formitem').each(function(item) { 32313 var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth; 32314 32315 maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth; 32316 labels.push(labelCtrl); 32317 }); 32318 32319 labelGap = self.settings.labelGap || 0; 32320 32321 i = labels.length; 32322 while (i--) { 32323 labels[i].settings.minWidth = maxLabelWidth + labelGap; 32324 } 32325 }, 32326 32327 /** 32328 * Getter/setter for the visibility state. 32329 * 32330 * @method visible 32331 * @param {Boolean} [state] True/false state to show/hide. 32332 * @return {tinymce.ui.Form|Boolean} True/false state or current control. 32333 */ 32334 visible: function(state) { 32335 var val = this._super(state); 32336 32337 if (state === true && this._rendered) { 32338 this.recalcLabels(); 32339 } 32340 32341 return val; 32342 }, 32343 32344 /** 32345 * Fires a submit event with the serialized form. 32346 * 32347 * @method submit 32348 * @return {Object} Event arguments object. 32349 */ 32350 submit: function() { 32351 return this.fire('submit', {data: this.toJSON()}); 32352 }, 32353 32354 /** 32355 * Post render method. Called after the control has been rendered to the target. 32356 * 32357 * @method postRender 32358 * @return {tinymce.ui.ComboBox} Current combobox instance. 32359 */ 32360 postRender: function() { 32361 var self = this; 32362 32363 self._super(); 32364 self.recalcLabels(); 32365 self.fromJSON(self.settings.data); 32366 } 32367 }); 32368 }); 32369 32370 // Included from: js/tinymce/classes/ui/FieldSet.js 32371 32372 /** 32373 * FieldSet.js 32374 * 32375 * Copyright, Moxiecode Systems AB 32376 * Released under LGPL License. 32377 * 32378 * License: http://www.tinymce.com/license 32379 * Contributing: http://www.tinymce.com/contributing 32380 */ 32381 32382 /** 32383 * This class creates fieldset containers. 32384 * 32385 * @-x-less FieldSet.less 32386 * @class tinymce.ui.FieldSet 32387 * @extends tinymce.ui.Form 32388 */ 32389 define("tinymce/ui/FieldSet", [ 32390 "tinymce/ui/Form" 32391 ], function(Form) { 32392 "use strict"; 32393 32394 return Form.extend({ 32395 Defaults: { 32396 containerCls: 'fieldset', 32397 layout: 'flex', 32398 direction: 'column', 32399 align: 'stretch', 32400 flex: 1, 32401 padding: "25 15 5 15", 32402 labelGap: 30, 32403 spacing: 10, 32404 border: 1 32405 }, 32406 32407 /** 32408 * Renders the control as a HTML string. 32409 * 32410 * @method renderHtml 32411 * @return {String} HTML representing the control. 32412 */ 32413 renderHtml: function() { 32414 var self = this, layout = self._layout, prefix = self.classPrefix; 32415 32416 self.preRender(); 32417 layout.preRender(self); 32418 32419 return ( 32420 '<fieldset id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 32421 (self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' + 32422 self.settings.title + '</legend>') : '') + 32423 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 32424 (self.settings.html || '') + layout.renderHtml(self) + 32425 '</div>' + 32426 '</fieldset>' 32427 ); 32428 } 32429 }); 32430 }); 32431 32432 // Included from: js/tinymce/classes/ui/FilePicker.js 32433 32434 /** 32435 * FilePicker.js 32436 * 32437 * Copyright, Moxiecode Systems AB 32438 * Released under LGPL License. 32439 * 32440 * License: http://www.tinymce.com/license 32441 * Contributing: http://www.tinymce.com/contributing 32442 */ 32443 32444 /*global tinymce:true */ 32445 32446 /** 32447 * This class creates a file picker control. 32448 * 32449 * @class tinymce.ui.FilePicker 32450 * @extends tinymce.ui.ComboBox 32451 */ 32452 define("tinymce/ui/FilePicker", [ 32453 "tinymce/ui/ComboBox", 32454 "tinymce/util/Tools" 32455 ], function(ComboBox, Tools) { 32456 "use strict"; 32457 32458 return ComboBox.extend({ 32459 /** 32460 * Constructs a new control instance with the specified settings. 32461 * 32462 * @constructor 32463 * @param {Object} settings Name/value object with settings. 32464 */ 32465 init: function(settings) { 32466 var self = this, editor = tinymce.activeEditor, editorSettings = editor.settings; 32467 var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes; 32468 32469 settings.spellcheck = false; 32470 32471 fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types; 32472 if (fileBrowserCallbackTypes) { 32473 fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/); 32474 } 32475 32476 if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype]) { 32477 fileBrowserCallback = editorSettings.file_picker_callback; 32478 if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) { 32479 actionCallback = function() { 32480 var meta = self.fire('beforecall').meta; 32481 32482 meta = Tools.extend({filetype: settings.filetype}, meta); 32483 32484 // file_picker_callback(callback, currentValue, metaData) 32485 fileBrowserCallback.call( 32486 editor, 32487 function(value, meta) { 32488 self.value(value).fire('change', {meta: meta}); 32489 }, 32490 self.value(), 32491 meta 32492 ); 32493 }; 32494 } else { 32495 // Legacy callback: file_picker_callback(id, currentValue, filetype, window) 32496 fileBrowserCallback = editorSettings.file_browser_callback; 32497 if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) { 32498 actionCallback = function() { 32499 fileBrowserCallback( 32500 self.getEl('inp').id, 32501 self.value(), 32502 settings.filetype, 32503 window 32504 ); 32505 }; 32506 } 32507 } 32508 } 32509 32510 if (actionCallback) { 32511 settings.icon = 'browse'; 32512 settings.onaction = actionCallback; 32513 } 32514 32515 self._super(settings); 32516 } 32517 }); 32518 }); 32519 32520 // Included from: js/tinymce/classes/ui/FitLayout.js 32521 32522 /** 32523 * FitLayout.js 32524 * 32525 * Copyright, Moxiecode Systems AB 32526 * Released under LGPL License. 32527 * 32528 * License: http://www.tinymce.com/license 32529 * Contributing: http://www.tinymce.com/contributing 32530 */ 32531 32532 /** 32533 * This layout manager will resize the control to be the size of it's parent container. 32534 * In other words width: 100% and height: 100%. 32535 * 32536 * @-x-less FitLayout.less 32537 * @class tinymce.ui.FitLayout 32538 * @extends tinymce.ui.AbsoluteLayout 32539 */ 32540 define("tinymce/ui/FitLayout", [ 32541 "tinymce/ui/AbsoluteLayout" 32542 ], function(AbsoluteLayout) { 32543 "use strict"; 32544 32545 return AbsoluteLayout.extend({ 32546 /** 32547 * Recalculates the positions of the controls in the specified container. 32548 * 32549 * @method recalc 32550 * @param {tinymce.ui.Container} container Container instance to recalc. 32551 */ 32552 recalc: function(container) { 32553 var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox(); 32554 32555 container.items().filter(':visible').each(function(ctrl) { 32556 ctrl.layoutRect({ 32557 x: paddingBox.left, 32558 y: paddingBox.top, 32559 w: contLayoutRect.innerW - paddingBox.right - paddingBox.left, 32560 h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom 32561 }); 32562 32563 if (ctrl.recalc) { 32564 ctrl.recalc(); 32565 } 32566 }); 32567 } 32568 }); 32569 }); 32570 32571 // Included from: js/tinymce/classes/ui/FlexLayout.js 32572 32573 /** 32574 * FlexLayout.js 32575 * 32576 * Copyright, Moxiecode Systems AB 32577 * Released under LGPL License. 32578 * 32579 * License: http://www.tinymce.com/license 32580 * Contributing: http://www.tinymce.com/contributing 32581 */ 32582 32583 /** 32584 * This layout manager works similar to the CSS flex box. 32585 * 32586 * @setting {String} direction row|row-reverse|column|column-reverse 32587 * @setting {Number} flex A positive-number to flex by. 32588 * @setting {String} align start|end|center|stretch 32589 * @setting {String} pack start|end|justify 32590 * 32591 * @class tinymce.ui.FlexLayout 32592 * @extends tinymce.ui.AbsoluteLayout 32593 */ 32594 define("tinymce/ui/FlexLayout", [ 32595 "tinymce/ui/AbsoluteLayout" 32596 ], function(AbsoluteLayout) { 32597 "use strict"; 32598 32599 return AbsoluteLayout.extend({ 32600 /** 32601 * Recalculates the positions of the controls in the specified container. 32602 * 32603 * @method recalc 32604 * @param {tinymce.ui.Container} container Container instance to recalc. 32605 */ 32606 recalc: function(container) { 32607 // A ton of variables, needs to be in the same scope for performance 32608 var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction; 32609 var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos; 32610 var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName; 32611 var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName; 32612 var alignDeltaSizeName, alignContentSizeName; 32613 var max = Math.max, min = Math.min; 32614 32615 // Get container items, properties and settings 32616 items = container.items().filter(':visible'); 32617 contLayoutRect = container.layoutRect(); 32618 contPaddingBox = container._paddingBox; 32619 contSettings = container.settings; 32620 direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction; 32621 align = contSettings.align; 32622 pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack; 32623 spacing = contSettings.spacing || 0; 32624 32625 if (direction == "row-reversed" || direction == "column-reverse") { 32626 items = items.set(items.toArray().reverse()); 32627 direction = direction.split('-')[0]; 32628 } 32629 32630 // Setup axis variable name for row/column direction since the calculations is the same 32631 if (direction == "column") { 32632 posName = "y"; 32633 sizeName = "h"; 32634 minSizeName = "minH"; 32635 maxSizeName = "maxH"; 32636 innerSizeName = "innerH"; 32637 beforeName = 'top'; 32638 deltaSizeName = "deltaH"; 32639 contentSizeName = "contentH"; 32640 32641 alignBeforeName = "left"; 32642 alignSizeName = "w"; 32643 alignAxisName = "x"; 32644 alignInnerSizeName = "innerW"; 32645 alignMinSizeName = "minW"; 32646 alignAfterName = "right"; 32647 alignDeltaSizeName = "deltaW"; 32648 alignContentSizeName = "contentW"; 32649 } else { 32650 posName = "x"; 32651 sizeName = "w"; 32652 minSizeName = "minW"; 32653 maxSizeName = "maxW"; 32654 innerSizeName = "innerW"; 32655 beforeName = 'left'; 32656 deltaSizeName = "deltaW"; 32657 contentSizeName = "contentW"; 32658 32659 alignBeforeName = "top"; 32660 alignSizeName = "h"; 32661 alignAxisName = "y"; 32662 alignInnerSizeName = "innerH"; 32663 alignMinSizeName = "minH"; 32664 alignAfterName = "bottom"; 32665 alignDeltaSizeName = "deltaH"; 32666 alignContentSizeName = "contentH"; 32667 } 32668 32669 // Figure out total flex, availableSpace and collect any max size elements 32670 availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName]; 32671 maxAlignEndPos = totalFlex = 0; 32672 for (i = 0, l = items.length; i < l; i++) { 32673 ctrl = items[i]; 32674 ctrlLayoutRect = ctrl.layoutRect(); 32675 ctrlSettings = ctrl.settings; 32676 flex = ctrlSettings.flex; 32677 availableSpace -= (i < l - 1 ? spacing : 0); 32678 32679 if (flex > 0) { 32680 totalFlex += flex; 32681 32682 // Flexed item has a max size then we need to check if we will hit that size 32683 if (ctrlLayoutRect[maxSizeName]) { 32684 maxSizeItems.push(ctrl); 32685 } 32686 32687 ctrlLayoutRect.flex = flex; 32688 } 32689 32690 availableSpace -= ctrlLayoutRect[minSizeName]; 32691 32692 // Calculate the align end position to be used to check for overflow/underflow 32693 size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName]; 32694 if (size > maxAlignEndPos) { 32695 maxAlignEndPos = size; 32696 } 32697 } 32698 32699 // Calculate minW/minH 32700 rect = {}; 32701 if (availableSpace < 0) { 32702 rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName]; 32703 } else { 32704 rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName]; 32705 } 32706 32707 rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName]; 32708 32709 rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace; 32710 rect[alignContentSizeName] = maxAlignEndPos; 32711 rect.minW = min(rect.minW, contLayoutRect.maxW); 32712 rect.minH = min(rect.minH, contLayoutRect.maxH); 32713 rect.minW = max(rect.minW, contLayoutRect.startMinWidth); 32714 rect.minH = max(rect.minH, contLayoutRect.startMinHeight); 32715 32716 // Resize container container if minSize was changed 32717 if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { 32718 rect.w = rect.minW; 32719 rect.h = rect.minH; 32720 32721 container.layoutRect(rect); 32722 this.recalc(container); 32723 32724 // Forced recalc for example if items are hidden/shown 32725 if (container._lastRect === null) { 32726 var parentCtrl = container.parent(); 32727 if (parentCtrl) { 32728 parentCtrl._lastRect = null; 32729 parentCtrl.recalc(); 32730 } 32731 } 32732 32733 return; 32734 } 32735 32736 // Handle max size elements, check if they will become to wide with current options 32737 ratio = availableSpace / totalFlex; 32738 for (i = 0, l = maxSizeItems.length; i < l; i++) { 32739 ctrl = maxSizeItems[i]; 32740 ctrlLayoutRect = ctrl.layoutRect(); 32741 maxSize = ctrlLayoutRect[maxSizeName]; 32742 size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio; 32743 32744 if (size > maxSize) { 32745 availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]); 32746 totalFlex -= ctrlLayoutRect.flex; 32747 ctrlLayoutRect.flex = 0; 32748 ctrlLayoutRect.maxFlexSize = maxSize; 32749 } else { 32750 ctrlLayoutRect.maxFlexSize = 0; 32751 } 32752 } 32753 32754 // Setup new ratio, target layout rect, start position 32755 ratio = availableSpace / totalFlex; 32756 pos = contPaddingBox[beforeName]; 32757 rect = {}; 32758 32759 // Handle pack setting moves the start position to end, center 32760 if (totalFlex === 0) { 32761 if (pack == "end") { 32762 pos = availableSpace + contPaddingBox[beforeName]; 32763 } else if (pack == "center") { 32764 pos = Math.round( 32765 (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2) 32766 ) + contPaddingBox[beforeName]; 32767 32768 if (pos < 0) { 32769 pos = contPaddingBox[beforeName]; 32770 } 32771 } else if (pack == "justify") { 32772 pos = contPaddingBox[beforeName]; 32773 spacing = Math.floor(availableSpace / (items.length - 1)); 32774 } 32775 } 32776 32777 // Default aligning (start) the other ones needs to be calculated while doing the layout 32778 rect[alignAxisName] = contPaddingBox[alignBeforeName]; 32779 32780 // Start laying out controls 32781 for (i = 0, l = items.length; i < l; i++) { 32782 ctrl = items[i]; 32783 ctrlLayoutRect = ctrl.layoutRect(); 32784 size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName]; 32785 32786 // Align the control on the other axis 32787 if (align === "center") { 32788 rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2)); 32789 } else if (align === "stretch") { 32790 rect[alignSizeName] = max( 32791 ctrlLayoutRect[alignMinSizeName] || 0, 32792 contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName] 32793 ); 32794 rect[alignAxisName] = contPaddingBox[alignBeforeName]; 32795 } else if (align === "end") { 32796 rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top; 32797 } 32798 32799 // Calculate new size based on flex 32800 if (ctrlLayoutRect.flex > 0) { 32801 size += ctrlLayoutRect.flex * ratio; 32802 } 32803 32804 rect[sizeName] = size; 32805 rect[posName] = pos; 32806 ctrl.layoutRect(rect); 32807 32808 // Recalculate containers 32809 if (ctrl.recalc) { 32810 ctrl.recalc(); 32811 } 32812 32813 // Move x/y position 32814 pos += size + spacing; 32815 } 32816 } 32817 }); 32818 }); 32819 32820 // Included from: js/tinymce/classes/ui/FlowLayout.js 32821 32822 /** 32823 * FlowLayout.js 32824 * 32825 * Copyright, Moxiecode Systems AB 32826 * Released under LGPL License. 32827 * 32828 * License: http://www.tinymce.com/license 32829 * Contributing: http://www.tinymce.com/contributing 32830 */ 32831 32832 /** 32833 * This layout manager will place the controls by using the browsers native layout. 32834 * 32835 * @-x-less FlowLayout.less 32836 * @class tinymce.ui.FlowLayout 32837 * @extends tinymce.ui.Layout 32838 */ 32839 define("tinymce/ui/FlowLayout", [ 32840 "tinymce/ui/Layout" 32841 ], function(Layout) { 32842 return Layout.extend({ 32843 Defaults: { 32844 containerClass: 'flow-layout', 32845 controlClass: 'flow-layout-item', 32846 endClass : 'break' 32847 }, 32848 32849 /** 32850 * Recalculates the positions of the controls in the specified container. 32851 * 32852 * @method recalc 32853 * @param {tinymce.ui.Container} container Container instance to recalc. 32854 */ 32855 recalc: function(container) { 32856 container.items().filter(':visible').each(function(ctrl) { 32857 if (ctrl.recalc) { 32858 ctrl.recalc(); 32859 } 32860 }); 32861 } 32862 }); 32863 }); 32864 32865 // Included from: js/tinymce/classes/ui/FormatControls.js 32866 32867 /** 32868 * FormatControls.js 32869 * 32870 * Copyright, Moxiecode Systems AB 32871 * Released under LGPL License. 32872 * 32873 * License: http://www.tinymce.com/license 32874 * Contributing: http://www.tinymce.com/contributing 32875 */ 32876 32877 /** 32878 * Internal class containing all TinyMCE specific control types such as 32879 * format listboxes, fontlist boxes, toolbar buttons etc. 32880 * 32881 * @class tinymce.ui.FormatControls 32882 */ 32883 define("tinymce/ui/FormatControls", [ 32884 "tinymce/ui/Control", 32885 "tinymce/ui/Widget", 32886 "tinymce/ui/FloatPanel", 32887 "tinymce/util/Tools", 32888 "tinymce/EditorManager", 32889 "tinymce/Env" 32890 ], function(Control, Widget, FloatPanel, Tools, EditorManager, Env) { 32891 var each = Tools.each; 32892 32893 EditorManager.on('AddEditor', function(e) { 32894 if (e.editor.rtl) { 32895 Control.rtl = true; 32896 } 32897 32898 registerControls(e.editor); 32899 }); 32900 32901 Control.translate = function(text) { 32902 return EditorManager.translate(text); 32903 }; 32904 32905 Widget.tooltips = !Env.iOS; 32906 32907 function registerControls(editor) { 32908 var formatMenu; 32909 32910 function createListBoxChangeHandler(items, formatName) { 32911 return function() { 32912 var self = this; 32913 32914 editor.on('nodeChange', function(e) { 32915 var formatter = editor.formatter; 32916 var value = null; 32917 32918 each(e.parents, function(node) { 32919 each(items, function(item) { 32920 if (formatName) { 32921 if (formatter.matchNode(node, formatName, {value: item.value})) { 32922 value = item.value; 32923 } 32924 } else { 32925 if (formatter.matchNode(node, item.value)) { 32926 value = item.value; 32927 } 32928 } 32929 32930 if (value) { 32931 return false; 32932 } 32933 }); 32934 32935 if (value) { 32936 return false; 32937 } 32938 }); 32939 32940 self.value(value); 32941 }); 32942 }; 32943 } 32944 32945 function createFormats(formats) { 32946 formats = formats.replace(/;$/, '').split(';'); 32947 32948 var i = formats.length; 32949 while (i--) { 32950 formats[i] = formats[i].split('='); 32951 } 32952 32953 return formats; 32954 } 32955 32956 function createFormatMenu() { 32957 var count = 0, newFormats = []; 32958 32959 var defaultStyleFormats = [ 32960 {title: 'Headings', items: [ 32961 {title: 'Heading 1', format: 'h1'}, 32962 {title: 'Heading 2', format: 'h2'}, 32963 {title: 'Heading 3', format: 'h3'}, 32964 {title: 'Heading 4', format: 'h4'}, 32965 {title: 'Heading 5', format: 'h5'}, 32966 {title: 'Heading 6', format: 'h6'} 32967 ]}, 32968 32969 {title: 'Inline', items: [ 32970 {title: 'Bold', icon: 'bold', format: 'bold'}, 32971 {title: 'Italic', icon: 'italic', format: 'italic'}, 32972 {title: 'Underline', icon: 'underline', format: 'underline'}, 32973 {title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'}, 32974 {title: 'Superscript', icon: 'superscript', format: 'superscript'}, 32975 {title: 'Subscript', icon: 'subscript', format: 'subscript'}, 32976 {title: 'Code', icon: 'code', format: 'code'} 32977 ]}, 32978 32979 {title: 'Blocks', items: [ 32980 {title: 'Paragraph', format: 'p'}, 32981 {title: 'Blockquote', format: 'blockquote'}, 32982 {title: 'Div', format: 'div'}, 32983 {title: 'Pre', format: 'pre'} 32984 ]}, 32985 32986 {title: 'Alignment', items: [ 32987 {title: 'Left', icon: 'alignleft', format: 'alignleft'}, 32988 {title: 'Center', icon: 'aligncenter', format: 'aligncenter'}, 32989 {title: 'Right', icon: 'alignright', format: 'alignright'}, 32990 {title: 'Justify', icon: 'alignjustify', format: 'alignjustify'} 32991 ]} 32992 ]; 32993 32994 function createMenu(formats) { 32995 var menu = []; 32996 32997 if (!formats) { 32998 return; 32999 } 33000 33001 each(formats, function(format) { 33002 var menuItem = { 33003 text: format.title, 33004 icon: format.icon 33005 }; 33006 33007 if (format.items) { 33008 menuItem.menu = createMenu(format.items); 33009 } else { 33010 var formatName = format.format || "custom" + count++; 33011 33012 if (!format.format) { 33013 format.name = formatName; 33014 newFormats.push(format); 33015 } 33016 33017 menuItem.format = formatName; 33018 menuItem.cmd = format.cmd; 33019 } 33020 33021 menu.push(menuItem); 33022 }); 33023 33024 return menu; 33025 } 33026 33027 function createStylesMenu() { 33028 var menu; 33029 33030 if (editor.settings.style_formats_merge) { 33031 if (editor.settings.style_formats) { 33032 menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats)); 33033 } else { 33034 menu = createMenu(defaultStyleFormats); 33035 } 33036 } else { 33037 menu = createMenu(editor.settings.style_formats || defaultStyleFormats); 33038 } 33039 33040 return menu; 33041 } 33042 33043 editor.on('init', function() { 33044 each(newFormats, function(format) { 33045 editor.formatter.register(format.name, format); 33046 }); 33047 }); 33048 33049 return { 33050 type: 'menu', 33051 items: createStylesMenu(), 33052 onPostRender: function(e) { 33053 editor.fire('renderFormatsMenu', {control: e.control}); 33054 }, 33055 itemDefaults: { 33056 preview: true, 33057 33058 textStyle: function() { 33059 if (this.settings.format) { 33060 return editor.formatter.getCssText(this.settings.format); 33061 } 33062 }, 33063 33064 onPostRender: function() { 33065 var self = this; 33066 33067 self.parent().on('show', function() { 33068 var formatName, command; 33069 33070 formatName = self.settings.format; 33071 if (formatName) { 33072 self.disabled(!editor.formatter.canApply(formatName)); 33073 self.active(editor.formatter.match(formatName)); 33074 } 33075 33076 command = self.settings.cmd; 33077 if (command) { 33078 self.active(editor.queryCommandState(command)); 33079 } 33080 }); 33081 }, 33082 33083 onclick: function() { 33084 if (this.settings.format) { 33085 toggleFormat(this.settings.format); 33086 } 33087 33088 if (this.settings.cmd) { 33089 editor.execCommand(this.settings.cmd); 33090 } 33091 } 33092 } 33093 }; 33094 } 33095 33096 formatMenu = createFormatMenu(); 33097 33098 // Simple format controls <control/format>:<UI text> 33099 each({ 33100 bold: 'Bold', 33101 italic: 'Italic', 33102 underline: 'Underline', 33103 strikethrough: 'Strikethrough', 33104 subscript: 'Subscript', 33105 superscript: 'Superscript' 33106 }, function(text, name) { 33107 editor.addButton(name, { 33108 tooltip: text, 33109 onPostRender: function() { 33110 var self = this; 33111 33112 // TODO: Fix this 33113 if (editor.formatter) { 33114 editor.formatter.formatChanged(name, function(state) { 33115 self.active(state); 33116 }); 33117 } else { 33118 editor.on('init', function() { 33119 editor.formatter.formatChanged(name, function(state) { 33120 self.active(state); 33121 }); 33122 }); 33123 } 33124 }, 33125 onclick: function() { 33126 toggleFormat(name); 33127 } 33128 }); 33129 }); 33130 33131 // Simple command controls <control>:[<UI text>,<Command>] 33132 each({ 33133 outdent: ['Decrease indent', 'Outdent'], 33134 indent: ['Increase indent', 'Indent'], 33135 cut: ['Cut', 'Cut'], 33136 copy: ['Copy', 'Copy'], 33137 paste: ['Paste', 'Paste'], 33138 help: ['Help', 'mceHelp'], 33139 selectall: ['Select all', 'SelectAll'], 33140 removeformat: ['Clear formatting', 'RemoveFormat'], 33141 visualaid: ['Visual aids', 'mceToggleVisualAid'], 33142 newdocument: ['New document', 'mceNewDocument'] 33143 }, function(item, name) { 33144 editor.addButton(name, { 33145 tooltip: item[0], 33146 cmd: item[1] 33147 }); 33148 }); 33149 33150 // Simple command controls with format state 33151 each({ 33152 blockquote: ['Blockquote', 'mceBlockQuote'], 33153 numlist: ['Numbered list', 'InsertOrderedList'], 33154 bullist: ['Bullet list', 'InsertUnorderedList'], 33155 subscript: ['Subscript', 'Subscript'], 33156 superscript: ['Superscript', 'Superscript'], 33157 alignleft: ['Align left', 'JustifyLeft'], 33158 aligncenter: ['Align center', 'JustifyCenter'], 33159 alignright: ['Align right', 'JustifyRight'], 33160 alignjustify: ['Justify', 'JustifyFull'] 33161 }, function(item, name) { 33162 editor.addButton(name, { 33163 tooltip: item[0], 33164 cmd: item[1], 33165 onPostRender: function() { 33166 var self = this; 33167 33168 // TODO: Fix this 33169 if (editor.formatter) { 33170 editor.formatter.formatChanged(name, function(state) { 33171 self.active(state); 33172 }); 33173 } else { 33174 editor.on('init', function() { 33175 editor.formatter.formatChanged(name, function(state) { 33176 self.active(state); 33177 }); 33178 }); 33179 } 33180 } 33181 }); 33182 }); 33183 33184 function toggleUndoRedoState(type) { 33185 return function() { 33186 var self = this; 33187 33188 type = type == 'redo' ? 'hasRedo' : 'hasUndo'; 33189 33190 function checkState() { 33191 return editor.undoManager ? editor.undoManager[type]() : false; 33192 } 33193 33194 self.disabled(!checkState()); 33195 editor.on('Undo Redo AddUndo TypingUndo ClearUndos', function() { 33196 self.disabled(!checkState()); 33197 }); 33198 }; 33199 } 33200 33201 function toggleVisualAidState() { 33202 var self = this; 33203 33204 editor.on('VisualAid', function(e) { 33205 self.active(e.hasVisual); 33206 }); 33207 33208 self.active(editor.hasVisual); 33209 } 33210 33211 editor.addButton('undo', { 33212 tooltip: 'Undo', 33213 onPostRender: toggleUndoRedoState('undo'), 33214 cmd: 'undo' 33215 }); 33216 33217 editor.addButton('redo', { 33218 tooltip: 'Redo', 33219 onPostRender: toggleUndoRedoState('redo'), 33220 cmd: 'redo' 33221 }); 33222 33223 editor.addMenuItem('newdocument', { 33224 text: 'New document', 33225 shortcut: 'Ctrl+N', 33226 icon: 'newdocument', 33227 cmd: 'mceNewDocument' 33228 }); 33229 33230 editor.addMenuItem('undo', { 33231 text: 'Undo', 33232 icon: 'undo', 33233 shortcut: 'Ctrl+Z', 33234 onPostRender: toggleUndoRedoState('undo'), 33235 cmd: 'undo' 33236 }); 33237 33238 editor.addMenuItem('redo', { 33239 text: 'Redo', 33240 icon: 'redo', 33241 shortcut: 'Ctrl+Y', 33242 onPostRender: toggleUndoRedoState('redo'), 33243 cmd: 'redo' 33244 }); 33245 33246 editor.addMenuItem('visualaid', { 33247 text: 'Visual aids', 33248 selectable: true, 33249 onPostRender: toggleVisualAidState, 33250 cmd: 'mceToggleVisualAid' 33251 }); 33252 33253 each({ 33254 cut: ['Cut', 'Cut', 'Ctrl+X'], 33255 copy: ['Copy', 'Copy', 'Ctrl+C'], 33256 paste: ['Paste', 'Paste', 'Ctrl+V'], 33257 selectall: ['Select all', 'SelectAll', 'Ctrl+A'], 33258 bold: ['Bold', 'Bold', 'Ctrl+B'], 33259 italic: ['Italic', 'Italic', 'Ctrl+I'], 33260 underline: ['Underline', 'Underline'], 33261 strikethrough: ['Strikethrough', 'Strikethrough'], 33262 subscript: ['Subscript', 'Subscript'], 33263 superscript: ['Superscript', 'Superscript'], 33264 removeformat: ['Clear formatting', 'RemoveFormat'] 33265 }, function(item, name) { 33266 editor.addMenuItem(name, { 33267 text: item[0], 33268 icon: name, 33269 shortcut: item[2], 33270 cmd: item[1] 33271 }); 33272 }); 33273 33274 editor.on('mousedown', function() { 33275 FloatPanel.hideAll(); 33276 }); 33277 33278 function toggleFormat(fmt) { 33279 if (fmt.control) { 33280 fmt = fmt.control.value(); 33281 } 33282 33283 if (fmt) { 33284 editor.execCommand('mceToggleFormat', false, fmt); 33285 } 33286 } 33287 33288 editor.addButton('styleselect', { 33289 type: 'menubutton', 33290 text: 'Formats', 33291 menu: formatMenu 33292 }); 33293 33294 editor.addButton('formatselect', function() { 33295 var items = [], blocks = createFormats(editor.settings.block_formats || 33296 'Paragraph=p;' + 33297 'Address=address;' + 33298 'Pre=pre;' + 33299 'Heading 1=h1;' + 33300 'Heading 2=h2;' + 33301 'Heading 3=h3;' + 33302 'Heading 4=h4;' + 33303 'Heading 5=h5;' + 33304 'Heading 6=h6' 33305 ); 33306 33307 each(blocks, function(block) { 33308 items.push({ 33309 text: block[0], 33310 value: block[1], 33311 textStyle: function() { 33312 return editor.formatter.getCssText(block[1]); 33313 } 33314 }); 33315 }); 33316 33317 return { 33318 type: 'listbox', 33319 text: blocks[0][0], 33320 values: items, 33321 fixedWidth: true, 33322 onselect: toggleFormat, 33323 onPostRender: createListBoxChangeHandler(items) 33324 }; 33325 }); 33326 33327 editor.addButton('fontselect', function() { 33328 var defaultFontsFormats = 33329 'Andale Mono=andale mono,times;' + 33330 'Arial=arial,helvetica,sans-serif;' + 33331 'Arial Black=arial black,avant garde;' + 33332 'Book Antiqua=book antiqua,palatino;' + 33333 'Comic Sans MS=comic sans ms,sans-serif;' + 33334 'Courier New=courier new,courier;' + 33335 'Georgia=georgia,palatino;' + 33336 'Helvetica=helvetica;' + 33337 'Impact=impact,chicago;' + 33338 'Symbol=symbol;' + 33339 'Tahoma=tahoma,arial,helvetica,sans-serif;' + 33340 'Terminal=terminal,monaco;' + 33341 'Times New Roman=times new roman,times;' + 33342 'Trebuchet MS=trebuchet ms,geneva;' + 33343 'Verdana=verdana,geneva;' + 33344 'Webdings=webdings;' + 33345 'Wingdings=wingdings,zapf dingbats'; 33346 33347 var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats); 33348 33349 each(fonts, function(font) { 33350 items.push({ 33351 text: {raw: font[0]}, 33352 value: font[1], 33353 textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : '' 33354 }); 33355 }); 33356 33357 return { 33358 type: 'listbox', 33359 text: 'Font Family', 33360 tooltip: 'Font Family', 33361 values: items, 33362 fixedWidth: true, 33363 onPostRender: createListBoxChangeHandler(items, 'fontname'), 33364 onselect: function(e) { 33365 if (e.control.settings.value) { 33366 editor.execCommand('FontName', false, e.control.settings.value); 33367 } 33368 } 33369 }; 33370 }); 33371 33372 editor.addButton('fontsizeselect', function() { 33373 var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt'; 33374 var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats; 33375 33376 each(fontsize_formats.split(' '), function(item) { 33377 var text = item, value = item; 33378 // Allow text=value font sizes. 33379 var values = item.split('='); 33380 if (values.length > 1) { 33381 text = values[0]; 33382 value = values[1]; 33383 } 33384 items.push({text: text, value: value}); 33385 }); 33386 33387 return { 33388 type: 'listbox', 33389 text: 'Font Sizes', 33390 tooltip: 'Font Sizes', 33391 values: items, 33392 fixedWidth: true, 33393 onPostRender: createListBoxChangeHandler(items, 'fontsize'), 33394 onclick: function(e) { 33395 if (e.control.settings.value) { 33396 editor.execCommand('FontSize', false, e.control.settings.value); 33397 } 33398 } 33399 }; 33400 }); 33401 33402 editor.addMenuItem('formats', { 33403 text: 'Formats', 33404 menu: formatMenu 33405 }); 33406 } 33407 }); 33408 33409 // Included from: js/tinymce/classes/ui/GridLayout.js 33410 33411 /** 33412 * GridLayout.js 33413 * 33414 * Copyright, Moxiecode Systems AB 33415 * Released under LGPL License. 33416 * 33417 * License: http://www.tinymce.com/license 33418 * Contributing: http://www.tinymce.com/contributing 33419 */ 33420 33421 /** 33422 * This layout manager places controls in a grid. 33423 * 33424 * @setting {Number} spacing Spacing between controls. 33425 * @setting {Number} spacingH Horizontal spacing between controls. 33426 * @setting {Number} spacingV Vertical spacing between controls. 33427 * @setting {Number} columns Number of columns to use. 33428 * @setting {String/Array} alignH start|end|center|stretch or array of values for each column. 33429 * @setting {String/Array} alignV start|end|center|stretch or array of values for each column. 33430 * @setting {String} pack start|end 33431 * 33432 * @class tinymce.ui.GridLayout 33433 * @extends tinymce.ui.AbsoluteLayout 33434 */ 33435 define("tinymce/ui/GridLayout", [ 33436 "tinymce/ui/AbsoluteLayout" 33437 ], function(AbsoluteLayout) { 33438 "use strict"; 33439 33440 return AbsoluteLayout.extend({ 33441 /** 33442 * Recalculates the positions of the controls in the specified container. 33443 * 33444 * @method recalc 33445 * @param {tinymce.ui.Container} container Container instance to recalc. 33446 */ 33447 recalc: function(container) { 33448 var settings = container.settings, rows, cols, items, contLayoutRect, width, height, rect, 33449 ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY, 33450 colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx; 33451 33452 // Get layout settings 33453 settings = container.settings; 33454 items = container.items().filter(':visible'); 33455 contLayoutRect = container.layoutRect(); 33456 cols = settings.columns || Math.ceil(Math.sqrt(items.length)); 33457 rows = Math.ceil(items.length / cols); 33458 spacingH = settings.spacingH || settings.spacing || 0; 33459 spacingV = settings.spacingV || settings.spacing || 0; 33460 alignH = settings.alignH || settings.align; 33461 alignV = settings.alignV || settings.align; 33462 contPaddingBox = container._paddingBox; 33463 reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl(); 33464 33465 if (alignH && typeof(alignH) == "string") { 33466 alignH = [alignH]; 33467 } 33468 33469 if (alignV && typeof(alignV) == "string") { 33470 alignV = [alignV]; 33471 } 33472 33473 // Zero padd columnWidths 33474 for (x = 0; x < cols; x++) { 33475 colWidths.push(0); 33476 } 33477 33478 // Zero padd rowHeights 33479 for (y = 0; y < rows; y++) { 33480 rowHeights.push(0); 33481 } 33482 33483 // Calculate columnWidths and rowHeights 33484 for (y = 0; y < rows; y++) { 33485 for (x = 0; x < cols; x++) { 33486 ctrl = items[y * cols + x]; 33487 33488 // Out of bounds 33489 if (!ctrl) { 33490 break; 33491 } 33492 33493 ctrlLayoutRect = ctrl.layoutRect(); 33494 ctrlMinWidth = ctrlLayoutRect.minW; 33495 ctrlMinHeight = ctrlLayoutRect.minH; 33496 33497 colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x]; 33498 rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y]; 33499 } 33500 } 33501 33502 // Calculate maxX 33503 availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right; 33504 for (maxX = 0, x = 0; x < cols; x++) { 33505 maxX += colWidths[x] + (x > 0 ? spacingH : 0); 33506 availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x]; 33507 } 33508 33509 // Calculate maxY 33510 availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom; 33511 for (maxY = 0, y = 0; y < rows; y++) { 33512 maxY += rowHeights[y] + (y > 0 ? spacingV : 0); 33513 availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y]; 33514 } 33515 33516 maxX += contPaddingBox.left + contPaddingBox.right; 33517 maxY += contPaddingBox.top + contPaddingBox.bottom; 33518 33519 // Calculate minW/minH 33520 rect = {}; 33521 rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW); 33522 rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH); 33523 33524 rect.contentW = rect.minW - contLayoutRect.deltaW; 33525 rect.contentH = rect.minH - contLayoutRect.deltaH; 33526 rect.minW = Math.min(rect.minW, contLayoutRect.maxW); 33527 rect.minH = Math.min(rect.minH, contLayoutRect.maxH); 33528 rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth); 33529 rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight); 33530 33531 // Resize container container if minSize was changed 33532 if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { 33533 rect.w = rect.minW; 33534 rect.h = rect.minH; 33535 33536 container.layoutRect(rect); 33537 this.recalc(container); 33538 33539 // Forced recalc for example if items are hidden/shown 33540 if (container._lastRect === null) { 33541 var parentCtrl = container.parent(); 33542 if (parentCtrl) { 33543 parentCtrl._lastRect = null; 33544 parentCtrl.recalc(); 33545 } 33546 } 33547 33548 return; 33549 } 33550 33551 // Update contentW/contentH so absEnd moves correctly 33552 if (contLayoutRect.autoResize) { 33553 rect = container.layoutRect(rect); 33554 rect.contentW = rect.minW - contLayoutRect.deltaW; 33555 rect.contentH = rect.minH - contLayoutRect.deltaH; 33556 } 33557 33558 var flexV; 33559 33560 if (settings.packV == 'start') { 33561 flexV = 0; 33562 } else { 33563 flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0; 33564 } 33565 33566 // Calculate totalFlex 33567 var totalFlex = 0; 33568 var flexWidths = settings.flexWidths; 33569 if (flexWidths) { 33570 for (x = 0; x < flexWidths.length; x++) { 33571 totalFlex += flexWidths[x]; 33572 } 33573 } else { 33574 totalFlex = cols; 33575 } 33576 33577 // Calculate new column widths based on flex values 33578 var ratio = availableWidth / totalFlex; 33579 for (x = 0; x < cols; x++) { 33580 colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio; 33581 } 33582 33583 // Move/resize controls 33584 posY = contPaddingBox.top; 33585 for (y = 0; y < rows; y++) { 33586 posX = contPaddingBox.left; 33587 height = rowHeights[y] + flexV; 33588 33589 for (x = 0; x < cols; x++) { 33590 if (reverseRows) { 33591 idx = y * cols + cols - 1 - x; 33592 } else { 33593 idx = y * cols + x; 33594 } 33595 33596 ctrl = items[idx]; 33597 33598 // No more controls to render then break 33599 if (!ctrl) { 33600 break; 33601 } 33602 33603 // Get control settings and calculate x, y 33604 ctrlSettings = ctrl.settings; 33605 ctrlLayoutRect = ctrl.layoutRect(); 33606 width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth); 33607 ctrlLayoutRect.x = posX; 33608 ctrlLayoutRect.y = posY; 33609 33610 // Align control horizontal 33611 align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null); 33612 if (align == "center") { 33613 ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2); 33614 } else if (align == "right") { 33615 ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w; 33616 } else if (align == "stretch") { 33617 ctrlLayoutRect.w = width; 33618 } 33619 33620 // Align control vertical 33621 align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null); 33622 if (align == "center") { 33623 ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2); 33624 } else if (align == "bottom") { 33625 ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h; 33626 } else if (align == "stretch") { 33627 ctrlLayoutRect.h = height; 33628 } 33629 33630 ctrl.layoutRect(ctrlLayoutRect); 33631 33632 posX += width + spacingH; 33633 33634 if (ctrl.recalc) { 33635 ctrl.recalc(); 33636 } 33637 } 33638 33639 posY += height + spacingV; 33640 } 33641 } 33642 }); 33643 }); 33644 33645 // Included from: js/tinymce/classes/ui/Iframe.js 33646 33647 /** 33648 * Iframe.js 33649 * 33650 * Copyright, Moxiecode Systems AB 33651 * Released under LGPL License. 33652 * 33653 * License: http://www.tinymce.com/license 33654 * Contributing: http://www.tinymce.com/contributing 33655 */ 33656 33657 /*jshint scripturl:true */ 33658 33659 /** 33660 * This class creates an iframe. 33661 * 33662 * @setting {String} url Url to open in the iframe. 33663 * 33664 * @-x-less Iframe.less 33665 * @class tinymce.ui.Iframe 33666 * @extends tinymce.ui.Widget 33667 */ 33668 define("tinymce/ui/Iframe", [ 33669 "tinymce/ui/Widget" 33670 ], function(Widget) { 33671 "use strict"; 33672 33673 return Widget.extend({ 33674 /** 33675 * Renders the control as a HTML string. 33676 * 33677 * @method renderHtml 33678 * @return {String} HTML representing the control. 33679 */ 33680 renderHtml: function() { 33681 var self = this; 33682 33683 self.addClass('iframe'); 33684 self.canFocus = false; 33685 33686 /*eslint no-script-url:0 */ 33687 return ( 33688 '<iframe id="' + self._id + '" class="' + self.classes() + '" tabindex="-1" src="' + 33689 (self.settings.url || "javascript:\'\'") + '" frameborder="0"></iframe>' 33690 ); 33691 }, 33692 33693 /** 33694 * Setter for the iframe source. 33695 * 33696 * @method src 33697 * @param {String} src Source URL for iframe. 33698 */ 33699 src: function(src) { 33700 this.getEl().src = src; 33701 }, 33702 33703 /** 33704 * Inner HTML for the iframe. 33705 * 33706 * @method html 33707 * @param {String} html HTML string to set as HTML inside the iframe. 33708 * @param {function} callback Optional callback to execute when the iframe body is filled with contents. 33709 * @return {tinymce.ui.Iframe} Current iframe control. 33710 */ 33711 html: function(html, callback) { 33712 var self = this, body = this.getEl().contentWindow.document.body; 33713 33714 // Wait for iframe to initialize IE 10 takes time 33715 if (!body) { 33716 setTimeout(function() { 33717 self.html(html); 33718 }, 0); 33719 } else { 33720 body.innerHTML = html; 33721 33722 if (callback) { 33723 callback(); 33724 } 33725 } 33726 33727 return this; 33728 } 33729 }); 33730 }); 33731 33732 // Included from: js/tinymce/classes/ui/Label.js 33733 33734 /** 33735 * Label.js 33736 * 33737 * Copyright, Moxiecode Systems AB 33738 * Released under LGPL License. 33739 * 33740 * License: http://www.tinymce.com/license 33741 * Contributing: http://www.tinymce.com/contributing 33742 */ 33743 33744 /** 33745 * This class creates a label element. A label is a simple text control 33746 * that can be bound to other controls. 33747 * 33748 * @-x-less Label.less 33749 * @class tinymce.ui.Label 33750 * @extends tinymce.ui.Widget 33751 */ 33752 define("tinymce/ui/Label", [ 33753 "tinymce/ui/Widget", 33754 "tinymce/ui/DomUtils" 33755 ], function(Widget, DomUtils) { 33756 "use strict"; 33757 33758 return Widget.extend({ 33759 /** 33760 * Constructs a instance with the specified settings. 33761 * 33762 * @constructor 33763 * @param {Object} settings Name/value object with settings. 33764 * @param {Boolean} multiline Multiline label. 33765 */ 33766 init: function(settings) { 33767 var self = this; 33768 33769 self._super(settings); 33770 self.addClass('widget'); 33771 self.addClass('label'); 33772 self.canFocus = false; 33773 33774 if (settings.multiline) { 33775 self.addClass('autoscroll'); 33776 } 33777 33778 if (settings.strong) { 33779 self.addClass('strong'); 33780 } 33781 }, 33782 33783 /** 33784 * Initializes the current controls layout rect. 33785 * This will be executed by the layout managers to determine the 33786 * default minWidth/minHeight etc. 33787 * 33788 * @method initLayoutRect 33789 * @return {Object} Layout rect instance. 33790 */ 33791 initLayoutRect: function() { 33792 var self = this, layoutRect = self._super(); 33793 33794 if (self.settings.multiline) { 33795 var size = DomUtils.getSize(self.getEl()); 33796 33797 // Check if the text fits within maxW if not then try word wrapping it 33798 if (size.width > layoutRect.maxW) { 33799 layoutRect.minW = layoutRect.maxW; 33800 self.addClass('multiline'); 33801 } 33802 33803 self.getEl().style.width = layoutRect.minW + 'px'; 33804 layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height); 33805 } 33806 33807 return layoutRect; 33808 }, 33809 33810 /** 33811 * Repaints the control after a layout operation. 33812 * 33813 * @method repaint 33814 */ 33815 repaint: function() { 33816 var self = this; 33817 33818 if (!self.settings.multiline) { 33819 self.getEl().style.lineHeight = self.layoutRect().h + 'px'; 33820 } 33821 33822 return self._super(); 33823 }, 33824 33825 /** 33826 * Sets/gets the current label text. 33827 * 33828 * @method text 33829 * @param {String} [text] New label text. 33830 * @return {String|tinymce.ui.Label} Current text or current label instance. 33831 */ 33832 text: function(text) { 33833 var self = this; 33834 33835 if (self._rendered && text) { 33836 this.innerHtml(self.encode(text)); 33837 } 33838 33839 return self._super(text); 33840 }, 33841 33842 /** 33843 * Renders the control as a HTML string. 33844 * 33845 * @method renderHtml 33846 * @return {String} HTML representing the control. 33847 */ 33848 renderHtml: function() { 33849 var self = this, forId = self.settings.forId; 33850 33851 return ( 33852 '<label id="' + self._id + '" class="' + self.classes() + '"' + (forId ? ' for="' + forId + '"' : '') + '>' + 33853 self.encode(self._text) + 33854 '</label>' 33855 ); 33856 } 33857 }); 33858 }); 33859 33860 // Included from: js/tinymce/classes/ui/MenuButton.js 33861 33862 /** 33863 * MenuButton.js 33864 * 33865 * Copyright, Moxiecode Systems AB 33866 * Released under LGPL License. 33867 * 33868 * License: http://www.tinymce.com/license 33869 * Contributing: http://www.tinymce.com/contributing 33870 */ 33871 33872 /** 33873 * Creates a new menu button. 33874 * 33875 * @-x-less MenuButton.less 33876 * @class tinymce.ui.MenuButton 33877 * @extends tinymce.ui.Button 33878 */ 33879 define("tinymce/ui/MenuButton", [ 33880 "tinymce/ui/Button", 33881 "tinymce/ui/Factory" 33882 ], function(Button, Factory) { 33883 "use strict"; 33884 33885 // TODO: Maybe add as some global function 33886 function isChildOf(node, parent) { 33887 while (node) { 33888 if (parent === node) { 33889 return true; 33890 } 33891 33892 node = node.parentNode; 33893 } 33894 33895 return false; 33896 } 33897 33898 var MenuButton = Button.extend({ 33899 /** 33900 * Constructs a instance with the specified settings. 33901 * 33902 * @constructor 33903 * @param {Object} settings Name/value object with settings. 33904 */ 33905 init: function(settings) { 33906 var self = this; 33907 33908 self._renderOpen = true; 33909 self._super(settings); 33910 33911 self.addClass('menubtn'); 33912 33913 if (settings.fixedWidth) { 33914 self.addClass('fixed-width'); 33915 } 33916 33917 self.aria('haspopup', true); 33918 self.hasPopup = true; 33919 }, 33920 33921 /** 33922 * Shows the menu for the button. 33923 * 33924 * @method showMenu 33925 */ 33926 showMenu: function() { 33927 var self = this, settings = self.settings, menu; 33928 33929 if (self.menu && self.menu.visible()) { 33930 return self.hideMenu(); 33931 } 33932 33933 if (!self.menu) { 33934 menu = settings.menu || []; 33935 33936 // Is menu array then auto constuct menu control 33937 if (menu.length) { 33938 menu = { 33939 type: 'menu', 33940 items: menu 33941 }; 33942 } else { 33943 menu.type = menu.type || 'menu'; 33944 } 33945 33946 self.menu = Factory.create(menu).parent(self).renderTo(); 33947 self.fire('createmenu'); 33948 self.menu.reflow(); 33949 self.menu.on('cancel', function(e) { 33950 if (e.control.parent() === self.menu) { 33951 e.stopPropagation(); 33952 e.preventDefault(); 33953 self.focus(); 33954 self.hideMenu(); 33955 } 33956 }); 33957 33958 // Move focus to button when a menu item is selected/clicked 33959 self.menu.on('select', function() { 33960 self.focus(); 33961 }); 33962 33963 self.menu.on('show hide', function(e) { 33964 if (e.control == self.menu) { 33965 self.activeMenu(e.type == 'show'); 33966 } 33967 33968 self.aria('expanded', e.type == 'show'); 33969 }).fire('show'); 33970 } 33971 33972 self.menu.show(); 33973 self.menu.layoutRect({w: self.layoutRect().w}); 33974 self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); 33975 }, 33976 33977 /** 33978 * Hides the menu for the button. 33979 * 33980 * @method hideMenu 33981 */ 33982 hideMenu: function() { 33983 var self = this; 33984 33985 if (self.menu) { 33986 self.menu.items().each(function(item) { 33987 if (item.hideMenu) { 33988 item.hideMenu(); 33989 } 33990 }); 33991 33992 self.menu.hide(); 33993 } 33994 }, 33995 33996 /** 33997 * Sets the active menu state. 33998 * 33999 * @private 34000 */ 34001 activeMenu: function(state) { 34002 this.toggleClass('active', state); 34003 }, 34004 34005 /** 34006 * Renders the control as a HTML string. 34007 * 34008 * @method renderHtml 34009 * @return {String} HTML representing the control. 34010 */ 34011 renderHtml: function() { 34012 var self = this, id = self._id, prefix = self.classPrefix; 34013 var icon = self.settings.icon, image; 34014 34015 image = self.settings.image; 34016 if (image) { 34017 icon = 'none'; 34018 34019 // Support for [high dpi, low dpi] image sources 34020 if (typeof image != "string") { 34021 image = window.getSelection ? image[0] : image[1]; 34022 } 34023 34024 image = ' style="background-image: url(\'' + image + '\')"'; 34025 } else { 34026 image = ''; 34027 } 34028 34029 icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; 34030 34031 var parentrolemap = { 34032 buttongroup: 'button', 34033 toolbar: 'button', 34034 menubar: 'menuitem' 34035 }; 34036 self.aria('role', parentrolemap[self.parent().type] || 'combobox'); 34037 34038 return ( 34039 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' + 34040 '<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' + 34041 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 34042 '<span>' + (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + '</span>' + 34043 ' <i class="' + prefix + 'caret"></i>' + 34044 '</button>' + 34045 '</div>' 34046 ); 34047 }, 34048 34049 /** 34050 * Gets invoked after the control has been rendered. 34051 * 34052 * @method postRender 34053 */ 34054 postRender: function() { 34055 var self = this; 34056 34057 self.on('click', function(e) { 34058 if (e.control === self && isChildOf(e.target, self.getEl())) { 34059 self.showMenu(); 34060 34061 if (e.aria) { 34062 self.menu.items()[0].focus(); 34063 } 34064 } 34065 }); 34066 34067 self.on('mouseenter', function(e) { 34068 var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu; 34069 34070 if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) { 34071 parent.items().filter('MenuButton').each(function(ctrl) { 34072 if (ctrl.hideMenu && ctrl != overCtrl) { 34073 if (ctrl.menu && ctrl.menu.visible()) { 34074 hasVisibleSiblingMenu = true; 34075 } 34076 34077 ctrl.hideMenu(); 34078 } 34079 }); 34080 34081 if (hasVisibleSiblingMenu) { 34082 overCtrl.focus(); // Fix for: #5887 34083 overCtrl.showMenu(); 34084 } 34085 } 34086 }); 34087 34088 return self._super(); 34089 }, 34090 34091 /** 34092 * Sets/gets the current button text. 34093 * 34094 * @method text 34095 * @param {String} [text] New button text. 34096 * @return {String|tinymce.ui.MenuButton} Current text or current MenuButton instance. 34097 */ 34098 text: function(text) { 34099 var self = this, i, children; 34100 34101 if (self._rendered) { 34102 children = self.getEl('open').getElementsByTagName('span'); 34103 for (i = 0; i < children.length; i++) { 34104 children[i].innerHTML = (self.settings.icon && text ? '\u00a0' : '') + self.encode(text); 34105 } 34106 } 34107 34108 return this._super(text); 34109 }, 34110 34111 /** 34112 * Removes the control and it's menus. 34113 * 34114 * @method remove 34115 */ 34116 remove: function() { 34117 this._super(); 34118 34119 if (this.menu) { 34120 this.menu.remove(); 34121 } 34122 } 34123 }); 34124 34125 return MenuButton; 34126 }); 34127 34128 // Included from: js/tinymce/classes/ui/ListBox.js 34129 34130 /** 34131 * ListBox.js 34132 * 34133 * Copyright, Moxiecode Systems AB 34134 * Released under LGPL License. 34135 * 34136 * License: http://www.tinymce.com/license 34137 * Contributing: http://www.tinymce.com/contributing 34138 */ 34139 34140 /** 34141 * Creates a new list box control. 34142 * 34143 * @-x-less ListBox.less 34144 * @class tinymce.ui.ListBox 34145 * @extends tinymce.ui.MenuButton 34146 */ 34147 define("tinymce/ui/ListBox", [ 34148 "tinymce/ui/MenuButton" 34149 ], function(MenuButton) { 34150 "use strict"; 34151 34152 return MenuButton.extend({ 34153 /** 34154 * Constructs a instance with the specified settings. 34155 * 34156 * @constructor 34157 * @param {Object} settings Name/value object with settings. 34158 * @setting {Array} values Array with values to add to list box. 34159 */ 34160 init: function(settings) { 34161 var self = this, values, selected, selectedText, lastItemCtrl; 34162 34163 function setSelected(menuValues) { 34164 // Try to find a selected value 34165 for (var i = 0; i < menuValues.length; i++) { 34166 selected = menuValues[i].selected || settings.value === menuValues[i].value; 34167 34168 if (selected) { 34169 selectedText = selectedText || menuValues[i].text; 34170 self._value = menuValues[i].value; 34171 break; 34172 } 34173 34174 // If the value has a submenu, try to find the selected values in that menu 34175 if (menuValues[i].menu) { 34176 setSelected(menuValues[i].menu); 34177 } 34178 } 34179 } 34180 34181 self._values = values = settings.values; 34182 if (values) { 34183 if (typeof settings.value != "undefined") { 34184 setSelected(values); 34185 } 34186 34187 // Default with first item 34188 if (!selected && values.length > 0) { 34189 selectedText = values[0].text; 34190 self._value = values[0].value; 34191 } 34192 34193 settings.menu = values; 34194 } 34195 34196 settings.text = settings.text || selectedText || values[0].text; 34197 34198 self._super(settings); 34199 self.addClass('listbox'); 34200 34201 self.on('select', function(e) { 34202 var ctrl = e.control; 34203 34204 if (lastItemCtrl) { 34205 e.lastControl = lastItemCtrl; 34206 } 34207 34208 if (settings.multiple) { 34209 ctrl.active(!ctrl.active()); 34210 } else { 34211 self.value(e.control.settings.value); 34212 } 34213 34214 lastItemCtrl = ctrl; 34215 }); 34216 }, 34217 34218 /** 34219 * Getter/setter function for the control value. 34220 * 34221 * @method value 34222 * @param {String} [value] Value to be set. 34223 * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation. 34224 */ 34225 value: function(value) { 34226 var self = this, active, selectedText, menu; 34227 34228 function activateByValue(menu, value) { 34229 menu.items().each(function(ctrl) { 34230 active = ctrl.value() === value; 34231 34232 if (active) { 34233 selectedText = selectedText || ctrl.text(); 34234 } 34235 34236 ctrl.active(active); 34237 34238 if (ctrl.menu) { 34239 activateByValue(ctrl.menu, value); 34240 } 34241 }); 34242 } 34243 34244 function setActiveValues(menuValues) { 34245 for (var i = 0; i < menuValues.length; i++) { 34246 active = menuValues[i].value == value; 34247 34248 if (active) { 34249 selectedText = selectedText || menuValues[i].text; 34250 } 34251 34252 menuValues[i].active = active; 34253 34254 if (menuValues[i].menu) { 34255 setActiveValues(menuValues[i].menu); 34256 } 34257 } 34258 } 34259 34260 if (typeof(value) != "undefined") { 34261 if (self.menu) { 34262 activateByValue(self.menu, value); 34263 } else { 34264 menu = self.settings.menu; 34265 setActiveValues(menu); 34266 } 34267 34268 self.text(selectedText || this.settings.text); 34269 } 34270 34271 return self._super(value); 34272 } 34273 }); 34274 }); 34275 34276 // Included from: js/tinymce/classes/ui/MenuItem.js 34277 34278 /** 34279 * MenuItem.js 34280 * 34281 * Copyright, Moxiecode Systems AB 34282 * Released under LGPL License. 34283 * 34284 * License: http://www.tinymce.com/license 34285 * Contributing: http://www.tinymce.com/contributing 34286 */ 34287 34288 /** 34289 * Creates a new menu item. 34290 * 34291 * @-x-less MenuItem.less 34292 * @class tinymce.ui.MenuItem 34293 * @extends tinymce.ui.Widget 34294 */ 34295 define("tinymce/ui/MenuItem", [ 34296 "tinymce/ui/Widget", 34297 "tinymce/ui/Factory", 34298 "tinymce/Env" 34299 ], function(Widget, Factory, Env) { 34300 "use strict"; 34301 34302 return Widget.extend({ 34303 Defaults: { 34304 border: 0, 34305 role: 'menuitem' 34306 }, 34307 34308 /** 34309 * Constructs a instance with the specified settings. 34310 * 34311 * @constructor 34312 * @param {Object} settings Name/value object with settings. 34313 * @setting {Boolean} selectable Selectable menu. 34314 * @setting {Array} menu Submenu array with items. 34315 * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X 34316 */ 34317 init: function(settings) { 34318 var self = this; 34319 34320 self.hasPopup = true; 34321 34322 self._super(settings); 34323 34324 settings = self.settings; 34325 34326 self.addClass('menu-item'); 34327 34328 if (settings.menu) { 34329 self.addClass('menu-item-expand'); 34330 } 34331 34332 if (settings.preview) { 34333 self.addClass('menu-item-preview'); 34334 } 34335 34336 if (self._text === '-' || self._text === '|') { 34337 self.addClass('menu-item-sep'); 34338 self.aria('role', 'separator'); 34339 self._text = '-'; 34340 } 34341 34342 if (settings.selectable) { 34343 self.aria('role', 'menuitemcheckbox'); 34344 self.addClass('menu-item-checkbox'); 34345 settings.icon = 'selected'; 34346 } 34347 34348 if (!settings.preview && !settings.selectable) { 34349 self.addClass('menu-item-normal'); 34350 } 34351 34352 self.on('mousedown', function(e) { 34353 e.preventDefault(); 34354 }); 34355 34356 if (settings.menu && !settings.ariaHideMenu) { 34357 self.aria('haspopup', true); 34358 } 34359 }, 34360 34361 /** 34362 * Returns true/false if the menuitem has sub menu. 34363 * 34364 * @method hasMenus 34365 * @return {Boolean} True/false state if it has submenu. 34366 */ 34367 hasMenus: function() { 34368 return !!this.settings.menu; 34369 }, 34370 34371 /** 34372 * Shows the menu for the menu item. 34373 * 34374 * @method showMenu 34375 */ 34376 showMenu: function() { 34377 var self = this, settings = self.settings, menu, parent = self.parent(); 34378 34379 parent.items().each(function(ctrl) { 34380 if (ctrl !== self) { 34381 ctrl.hideMenu(); 34382 } 34383 }); 34384 34385 if (settings.menu) { 34386 menu = self.menu; 34387 34388 if (!menu) { 34389 menu = settings.menu; 34390 34391 // Is menu array then auto constuct menu control 34392 if (menu.length) { 34393 menu = { 34394 type: 'menu', 34395 items: menu 34396 }; 34397 } else { 34398 menu.type = menu.type || 'menu'; 34399 } 34400 34401 if (parent.settings.itemDefaults) { 34402 menu.itemDefaults = parent.settings.itemDefaults; 34403 } 34404 34405 menu = self.menu = Factory.create(menu).parent(self).renderTo(); 34406 menu.reflow(); 34407 menu.on('cancel', function(e) { 34408 e.stopPropagation(); 34409 self.focus(); 34410 menu.hide(); 34411 }); 34412 menu.on('show hide', function(e) { 34413 e.control.items().each(function(ctrl) { 34414 ctrl.active(ctrl.settings.selected); 34415 }); 34416 }).fire('show'); 34417 34418 menu.on('hide', function(e) { 34419 if (e.control === menu) { 34420 self.removeClass('selected'); 34421 } 34422 }); 34423 34424 menu.submenu = true; 34425 } else { 34426 menu.show(); 34427 } 34428 34429 menu._parentMenu = parent; 34430 34431 menu.addClass('menu-sub'); 34432 34433 var rel = menu.testMoveRel( 34434 self.getEl(), 34435 self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br'] 34436 ); 34437 34438 menu.moveRel(self.getEl(), rel); 34439 menu.rel = rel; 34440 34441 rel = 'menu-sub-' + rel; 34442 menu.removeClass(menu._lastRel); 34443 menu.addClass(rel); 34444 menu._lastRel = rel; 34445 34446 self.addClass('selected'); 34447 self.aria('expanded', true); 34448 } 34449 }, 34450 34451 /** 34452 * Hides the menu for the menu item. 34453 * 34454 * @method hideMenu 34455 */ 34456 hideMenu: function() { 34457 var self = this; 34458 34459 if (self.menu) { 34460 self.menu.items().each(function(item) { 34461 if (item.hideMenu) { 34462 item.hideMenu(); 34463 } 34464 }); 34465 34466 self.menu.hide(); 34467 self.aria('expanded', false); 34468 } 34469 34470 return self; 34471 }, 34472 34473 /** 34474 * Renders the control as a HTML string. 34475 * 34476 * @method renderHtml 34477 * @return {String} HTML representing the control. 34478 */ 34479 renderHtml: function() { 34480 var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.encode(self._text); 34481 var icon = self.settings.icon, image = '', shortcut = settings.shortcut; 34482 34483 if (icon) { 34484 self.parent().addClass('menu-has-icons'); 34485 } 34486 34487 if (settings.image) { 34488 icon = 'none'; 34489 image = ' style="background-image: url(\'' + settings.image + '\')"'; 34490 } 34491 34492 if (shortcut && Env.mac) { 34493 // format shortcut for Mac 34494 shortcut = shortcut.replace(/ctrl\+alt\+/i, '⌥⌘'); // ctrl+cmd 34495 shortcut = shortcut.replace(/ctrl\+/i, '⌘'); // ctrl symbol 34496 shortcut = shortcut.replace(/alt\+/i, '⌥'); // cmd symbol 34497 shortcut = shortcut.replace(/shift\+/i, '⇧'); // shift symbol 34498 } 34499 34500 icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none'); 34501 34502 return ( 34503 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' + 34504 (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '') + 34505 (text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') + 34506 (shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') + 34507 (settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') + 34508 '</div>' 34509 ); 34510 }, 34511 34512 /** 34513 * Gets invoked after the control has been rendered. 34514 * 34515 * @method postRender 34516 */ 34517 postRender: function() { 34518 var self = this, settings = self.settings; 34519 34520 var textStyle = settings.textStyle; 34521 if (typeof(textStyle) == "function") { 34522 textStyle = textStyle.call(this); 34523 } 34524 34525 if (textStyle) { 34526 var textElm = self.getEl('text'); 34527 if (textElm) { 34528 textElm.setAttribute('style', textStyle); 34529 } 34530 } 34531 34532 self.on('mouseenter click', function(e) { 34533 if (e.control === self) { 34534 if (!settings.menu && e.type === 'click') { 34535 self.fire('select'); 34536 self.parent().hideAll(); 34537 } else { 34538 self.showMenu(); 34539 34540 if (e.aria) { 34541 self.menu.focus(true); 34542 } 34543 } 34544 } 34545 }); 34546 34547 self._super(); 34548 34549 return self; 34550 }, 34551 34552 active: function(state) { 34553 if (typeof(state) != "undefined") { 34554 this.aria('checked', state); 34555 } 34556 34557 return this._super(state); 34558 }, 34559 34560 /** 34561 * Removes the control and it's menus. 34562 * 34563 * @method remove 34564 */ 34565 remove: function() { 34566 this._super(); 34567 34568 if (this.menu) { 34569 this.menu.remove(); 34570 } 34571 } 34572 }); 34573 }); 34574 34575 // Included from: js/tinymce/classes/ui/Menu.js 34576 34577 /** 34578 * Menu.js 34579 * 34580 * Copyright, Moxiecode Systems AB 34581 * Released under LGPL License. 34582 * 34583 * License: http://www.tinymce.com/license 34584 * Contributing: http://www.tinymce.com/contributing 34585 */ 34586 34587 /** 34588 * Creates a new menu. 34589 * 34590 * @-x-less Menu.less 34591 * @class tinymce.ui.Menu 34592 * @extends tinymce.ui.FloatPanel 34593 */ 34594 define("tinymce/ui/Menu", [ 34595 "tinymce/ui/FloatPanel", 34596 "tinymce/ui/MenuItem", 34597 "tinymce/util/Tools" 34598 ], function(FloatPanel, MenuItem, Tools) { 34599 "use strict"; 34600 34601 var Menu = FloatPanel.extend({ 34602 Defaults: { 34603 defaultType: 'menuitem', 34604 border: 1, 34605 layout: 'stack', 34606 role: 'application', 34607 bodyRole: 'menu', 34608 ariaRoot: true 34609 }, 34610 34611 /** 34612 * Constructs a instance with the specified settings. 34613 * 34614 * @constructor 34615 * @param {Object} settings Name/value object with settings. 34616 */ 34617 init: function(settings) { 34618 var self = this; 34619 34620 settings.autohide = true; 34621 settings.constrainToViewport = true; 34622 34623 if (settings.itemDefaults) { 34624 var items = settings.items, i = items.length; 34625 34626 while (i--) { 34627 items[i] = Tools.extend({}, settings.itemDefaults, items[i]); 34628 } 34629 } 34630 34631 self._super(settings); 34632 self.addClass('menu'); 34633 }, 34634 34635 /** 34636 * Repaints the control after a layout operation. 34637 * 34638 * @method repaint 34639 */ 34640 repaint: function() { 34641 this.toggleClass('menu-align', true); 34642 34643 this._super(); 34644 34645 this.getEl().style.height = ''; 34646 this.getEl('body').style.height = ''; 34647 34648 return this; 34649 }, 34650 34651 /** 34652 * Hides/closes the menu. 34653 * 34654 * @method cancel 34655 */ 34656 cancel: function() { 34657 var self = this; 34658 34659 self.hideAll(); 34660 self.fire('select'); 34661 }, 34662 34663 /** 34664 * Hide menu and all sub menus. 34665 * 34666 * @method hideAll 34667 */ 34668 hideAll: function() { 34669 var self = this; 34670 34671 this.find('menuitem').exec('hideMenu'); 34672 34673 return self._super(); 34674 }, 34675 /* 34676 getContainerElm: function() { 34677 var doc = document, id = this.classPrefix + 'menucontainer'; 34678 34679 var elm = doc.getElementById(id); 34680 if (!elm) { 34681 elm = doc.createElement('div'); 34682 elm.id = id; 34683 elm.setAttribute('role', 'application'); 34684 elm.className = this.classPrefix + '-reset'; 34685 elm.style.position = 'absolute'; 34686 elm.style.top = elm.style.left = '0'; 34687 elm.style.overflow = 'visible'; 34688 doc.body.appendChild(elm); 34689 } 34690 34691 return elm; 34692 }, 34693 */ 34694 /** 34695 * Invoked before the menu is rendered. 34696 * 34697 * @method preRender 34698 */ 34699 preRender: function() { 34700 var self = this; 34701 34702 self.items().each(function(ctrl) { 34703 var settings = ctrl.settings; 34704 34705 if (settings.icon || settings.selectable) { 34706 self._hasIcons = true; 34707 return false; 34708 } 34709 }); 34710 34711 return self._super(); 34712 } 34713 }); 34714 34715 return Menu; 34716 }); 34717 34718 // Included from: js/tinymce/classes/ui/Toolbar.js 34719 34720 /** 34721 * Toolbar.js 34722 * 34723 * Copyright, Moxiecode Systems AB 34724 * Released under LGPL License. 34725 * 34726 * License: http://www.tinymce.com/license 34727 * Contributing: http://www.tinymce.com/contributing 34728 */ 34729 34730 /** 34731 * Creates a new toolbar. 34732 * 34733 * @class tinymce.ui.Toolbar 34734 * @extends tinymce.ui.Container 34735 */ 34736 define("tinymce/ui/Toolbar", [ 34737 "tinymce/ui/Container" 34738 ], function(Container) { 34739 "use strict"; 34740 34741 return Container.extend({ 34742 Defaults: { 34743 role: 'toolbar', 34744 layout: 'flow' 34745 }, 34746 34747 /** 34748 * Constructs a instance with the specified settings. 34749 * 34750 * @constructor 34751 * @param {Object} settings Name/value object with settings. 34752 */ 34753 init: function(settings) { 34754 var self = this; 34755 34756 self._super(settings); 34757 self.addClass('toolbar'); 34758 }, 34759 34760 /** 34761 * Called after the control has been rendered. 34762 * 34763 * @method postRender 34764 */ 34765 postRender: function() { 34766 var self = this; 34767 34768 self.items().addClass('toolbar-item'); 34769 34770 return self._super(); 34771 } 34772 }); 34773 }); 34774 34775 // Included from: js/tinymce/classes/ui/MenuBar.js 34776 34777 /** 34778 * MenuBar.js 34779 * 34780 * Copyright, Moxiecode Systems AB 34781 * Released under LGPL License. 34782 * 34783 * License: http://www.tinymce.com/license 34784 * Contributing: http://www.tinymce.com/contributing 34785 */ 34786 34787 /** 34788 * Creates a new menubar. 34789 * 34790 * @-x-less MenuBar.less 34791 * @class tinymce.ui.MenuBar 34792 * @extends tinymce.ui.Toolbar 34793 */ 34794 define("tinymce/ui/MenuBar", [ 34795 "tinymce/ui/Toolbar" 34796 ], function(Toolbar) { 34797 "use strict"; 34798 34799 return Toolbar.extend({ 34800 Defaults: { 34801 role: 'menubar', 34802 containerCls: 'menubar', 34803 ariaRoot: true, 34804 defaults: { 34805 type: 'menubutton' 34806 } 34807 } 34808 }); 34809 }); 34810 34811 // Included from: js/tinymce/classes/ui/Radio.js 34812 34813 /** 34814 * Radio.js 34815 * 34816 * Copyright, Moxiecode Systems AB 34817 * Released under LGPL License. 34818 * 34819 * License: http://www.tinymce.com/license 34820 * Contributing: http://www.tinymce.com/contributing 34821 */ 34822 34823 /** 34824 * Creates a new radio button. 34825 * 34826 * @-x-less Radio.less 34827 * @class tinymce.ui.Radio 34828 * @extends tinymce.ui.Checkbox 34829 */ 34830 define("tinymce/ui/Radio", [ 34831 "tinymce/ui/Checkbox" 34832 ], function(Checkbox) { 34833 "use strict"; 34834 34835 return Checkbox.extend({ 34836 Defaults: { 34837 classes: "radio", 34838 role: "radio" 34839 } 34840 }); 34841 }); 34842 34843 // Included from: js/tinymce/classes/ui/ResizeHandle.js 34844 34845 /** 34846 * ResizeHandle.js 34847 * 34848 * Copyright, Moxiecode Systems AB 34849 * Released under LGPL License. 34850 * 34851 * License: http://www.tinymce.com/license 34852 * Contributing: http://www.tinymce.com/contributing 34853 */ 34854 34855 /** 34856 * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events. 34857 * 34858 * @-x-less ResizeHandle.less 34859 * @class tinymce.ui.ResizeHandle 34860 * @extends tinymce.ui.Widget 34861 */ 34862 define("tinymce/ui/ResizeHandle", [ 34863 "tinymce/ui/Widget", 34864 "tinymce/ui/DragHelper" 34865 ], function(Widget, DragHelper) { 34866 "use strict"; 34867 34868 return Widget.extend({ 34869 /** 34870 * Renders the control as a HTML string. 34871 * 34872 * @method renderHtml 34873 * @return {String} HTML representing the control. 34874 */ 34875 renderHtml: function() { 34876 var self = this, prefix = self.classPrefix; 34877 34878 self.addClass('resizehandle'); 34879 34880 if (self.settings.direction == "both") { 34881 self.addClass('resizehandle-both'); 34882 } 34883 34884 self.canFocus = false; 34885 34886 return ( 34887 '<div id="' + self._id + '" class="' + self.classes() + '">' + 34888 '<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' + 34889 '</div>' 34890 ); 34891 }, 34892 34893 /** 34894 * Called after the control has been rendered. 34895 * 34896 * @method postRender 34897 */ 34898 postRender: function() { 34899 var self = this; 34900 34901 self._super(); 34902 34903 self.resizeDragHelper = new DragHelper(this._id, { 34904 start: function() { 34905 self.fire('ResizeStart'); 34906 }, 34907 34908 drag: function(e) { 34909 if (self.settings.direction != "both") { 34910 e.deltaX = 0; 34911 } 34912 34913 self.fire('Resize', e); 34914 }, 34915 34916 stop: function() { 34917 self.fire('ResizeEnd'); 34918 } 34919 }); 34920 }, 34921 34922 remove: function() { 34923 if (this.resizeDragHelper) { 34924 this.resizeDragHelper.destroy(); 34925 } 34926 34927 return this._super(); 34928 } 34929 }); 34930 }); 34931 34932 // Included from: js/tinymce/classes/ui/Spacer.js 34933 34934 /** 34935 * Spacer.js 34936 * 34937 * Copyright, Moxiecode Systems AB 34938 * Released under LGPL License. 34939 * 34940 * License: http://www.tinymce.com/license 34941 * Contributing: http://www.tinymce.com/contributing 34942 */ 34943 34944 /** 34945 * Creates a spacer. This control is used in flex layouts for example. 34946 * 34947 * @-x-less Spacer.less 34948 * @class tinymce.ui.Spacer 34949 * @extends tinymce.ui.Widget 34950 */ 34951 define("tinymce/ui/Spacer", [ 34952 "tinymce/ui/Widget" 34953 ], function(Widget) { 34954 "use strict"; 34955 34956 return Widget.extend({ 34957 /** 34958 * Renders the control as a HTML string. 34959 * 34960 * @method renderHtml 34961 * @return {String} HTML representing the control. 34962 */ 34963 renderHtml: function() { 34964 var self = this; 34965 34966 self.addClass('spacer'); 34967 self.canFocus = false; 34968 34969 return '<div id="' + self._id + '" class="' + self.classes() + '"></div>'; 34970 } 34971 }); 34972 }); 34973 34974 // Included from: js/tinymce/classes/ui/SplitButton.js 34975 34976 /** 34977 * SplitButton.js 34978 * 34979 * Copyright, Moxiecode Systems AB 34980 * Released under LGPL License. 34981 * 34982 * License: http://www.tinymce.com/license 34983 * Contributing: http://www.tinymce.com/contributing 34984 */ 34985 34986 /** 34987 * Creates a split button. 34988 * 34989 * @-x-less SplitButton.less 34990 * @class tinymce.ui.SplitButton 34991 * @extends tinymce.ui.MenuButton 34992 */ 34993 define("tinymce/ui/SplitButton", [ 34994 "tinymce/ui/MenuButton", 34995 "tinymce/ui/DomUtils" 34996 ], function(MenuButton, DomUtils) { 34997 return MenuButton.extend({ 34998 Defaults: { 34999 classes: "widget btn splitbtn", 35000 role: "button" 35001 }, 35002 35003 /** 35004 * Repaints the control after a layout operation. 35005 * 35006 * @method repaint 35007 */ 35008 repaint: function() { 35009 var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm; 35010 35011 self._super(); 35012 35013 mainButtonElm = elm.firstChild; 35014 menuButtonElm = elm.lastChild; 35015 35016 DomUtils.css(mainButtonElm, { 35017 width: rect.w - DomUtils.getSize(menuButtonElm).width, 35018 height: rect.h - 2 35019 }); 35020 35021 DomUtils.css(menuButtonElm, { 35022 height: rect.h - 2 35023 }); 35024 35025 return self; 35026 }, 35027 35028 /** 35029 * Sets the active menu state. 35030 * 35031 * @private 35032 */ 35033 activeMenu: function(state) { 35034 var self = this; 35035 35036 DomUtils.toggleClass(self.getEl().lastChild, self.classPrefix + 'active', state); 35037 }, 35038 35039 /** 35040 * Renders the control as a HTML string. 35041 * 35042 * @method renderHtml 35043 * @return {String} HTML representing the control. 35044 */ 35045 renderHtml: function() { 35046 var self = this, id = self._id, prefix = self.classPrefix, image; 35047 var icon = self.settings.icon; 35048 35049 image = self.settings.image; 35050 if (image) { 35051 icon = 'none'; 35052 35053 // Support for [high dpi, low dpi] image sources 35054 if (typeof image != "string") { 35055 image = window.getSelection ? image[0] : image[1]; 35056 } 35057 35058 image = ' style="background-image: url(\'' + image + '\')"'; 35059 } else { 35060 image = ''; 35061 } 35062 35063 icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; 35064 35065 return ( 35066 '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1">' + 35067 '<button type="button" hidefocus="1" tabindex="-1">' + 35068 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 35069 (self._text ? (icon ? ' ' : '') + self._text : '') + 35070 '</button>' + 35071 '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + 35072 //(icon ? '<i class="' + icon + '"></i>' : '') + 35073 (self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') + 35074 ' <i class="' + prefix + 'caret"></i>' + 35075 '</button>' + 35076 '</div>' 35077 ); 35078 }, 35079 35080 /** 35081 * Called after the control has been rendered. 35082 * 35083 * @method postRender 35084 */ 35085 postRender: function() { 35086 var self = this, onClickHandler = self.settings.onclick; 35087 35088 self.on('click', function(e) { 35089 var node = e.target; 35090 35091 if (e.control == this) { 35092 // Find clicks that is on the main button 35093 while (node) { 35094 if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) { 35095 e.stopImmediatePropagation(); 35096 onClickHandler.call(this, e); 35097 return; 35098 } 35099 35100 node = node.parentNode; 35101 } 35102 } 35103 }); 35104 35105 delete self.settings.onclick; 35106 35107 return self._super(); 35108 } 35109 }); 35110 }); 35111 35112 // Included from: js/tinymce/classes/ui/StackLayout.js 35113 35114 /** 35115 * StackLayout.js 35116 * 35117 * Copyright, Moxiecode Systems AB 35118 * Released under LGPL License. 35119 * 35120 * License: http://www.tinymce.com/license 35121 * Contributing: http://www.tinymce.com/contributing 35122 */ 35123 35124 /** 35125 * This layout uses the browsers layout when the items are blocks. 35126 * 35127 * @-x-less StackLayout.less 35128 * @class tinymce.ui.StackLayout 35129 * @extends tinymce.ui.FlowLayout 35130 */ 35131 define("tinymce/ui/StackLayout", [ 35132 "tinymce/ui/FlowLayout" 35133 ], function(FlowLayout) { 35134 "use strict"; 35135 35136 return FlowLayout.extend({ 35137 Defaults: { 35138 containerClass: 'stack-layout', 35139 controlClass: 'stack-layout-item', 35140 endClass : 'break' 35141 } 35142 }); 35143 }); 35144 35145 // Included from: js/tinymce/classes/ui/TabPanel.js 35146 35147 /** 35148 * TabPanel.js 35149 * 35150 * Copyright, Moxiecode Systems AB 35151 * Released under LGPL License. 35152 * 35153 * License: http://www.tinymce.com/license 35154 * Contributing: http://www.tinymce.com/contributing 35155 */ 35156 35157 /** 35158 * Creates a tab panel control. 35159 * 35160 * @-x-less TabPanel.less 35161 * @class tinymce.ui.TabPanel 35162 * @extends tinymce.ui.Panel 35163 * 35164 * @setting {Number} activeTab Active tab index. 35165 */ 35166 define("tinymce/ui/TabPanel", [ 35167 "tinymce/ui/Panel", 35168 "tinymce/ui/DomUtils" 35169 ], function(Panel, DomUtils) { 35170 "use strict"; 35171 35172 return Panel.extend({ 35173 Defaults: { 35174 layout: 'absolute', 35175 defaults: { 35176 type: 'panel' 35177 } 35178 }, 35179 35180 /** 35181 * Activates the specified tab by index. 35182 * 35183 * @method activateTab 35184 * @param {Number} idx Index of the tab to activate. 35185 */ 35186 activateTab: function(idx) { 35187 var activeTabElm; 35188 35189 if (this.activeTabId) { 35190 activeTabElm = this.getEl(this.activeTabId); 35191 DomUtils.removeClass(activeTabElm, this.classPrefix + 'active'); 35192 activeTabElm.setAttribute('aria-selected', "false"); 35193 } 35194 35195 this.activeTabId = 't' + idx; 35196 35197 activeTabElm = this.getEl('t' + idx); 35198 activeTabElm.setAttribute('aria-selected', "true"); 35199 DomUtils.addClass(activeTabElm, this.classPrefix + 'active'); 35200 35201 this.items()[idx].show().fire('showtab'); 35202 this.reflow(); 35203 35204 this.items().each(function(item, i) { 35205 if (idx != i) { 35206 item.hide(); 35207 } 35208 }); 35209 }, 35210 35211 /** 35212 * Renders the control as a HTML string. 35213 * 35214 * @method renderHtml 35215 * @return {String} HTML representing the control. 35216 */ 35217 renderHtml: function() { 35218 var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix; 35219 35220 self.preRender(); 35221 layout.preRender(self); 35222 35223 self.items().each(function(ctrl, i) { 35224 var id = self._id + '-t' + i; 35225 35226 ctrl.aria('role', 'tabpanel'); 35227 ctrl.aria('labelledby', id); 35228 35229 tabsHtml += ( 35230 '<div id="' + id + '" class="' + prefix + 'tab" ' + 35231 'unselectable="on" role="tab" aria-controls="' + ctrl._id + '" aria-selected="false" tabIndex="-1">' + 35232 self.encode(ctrl.settings.title) + 35233 '</div>' 35234 ); 35235 }); 35236 35237 return ( 35238 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 35239 '<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' + 35240 tabsHtml + 35241 '</div>' + 35242 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 35243 layout.renderHtml(self) + 35244 '</div>' + 35245 '</div>' 35246 ); 35247 }, 35248 35249 /** 35250 * Called after the control has been rendered. 35251 * 35252 * @method postRender 35253 */ 35254 postRender: function() { 35255 var self = this; 35256 35257 self._super(); 35258 35259 self.settings.activeTab = self.settings.activeTab || 0; 35260 self.activateTab(self.settings.activeTab); 35261 35262 this.on('click', function(e) { 35263 var targetParent = e.target.parentNode; 35264 35265 if (e.target.parentNode.id == self._id + '-head') { 35266 var i = targetParent.childNodes.length; 35267 35268 while (i--) { 35269 if (targetParent.childNodes[i] == e.target) { 35270 self.activateTab(i); 35271 } 35272 } 35273 } 35274 }); 35275 }, 35276 35277 /** 35278 * Initializes the current controls layout rect. 35279 * This will be executed by the layout managers to determine the 35280 * default minWidth/minHeight etc. 35281 * 35282 * @method initLayoutRect 35283 * @return {Object} Layout rect instance. 35284 */ 35285 initLayoutRect: function() { 35286 var self = this, rect, minW, minH; 35287 35288 minW = DomUtils.getSize(self.getEl('head')).width; 35289 minW = minW < 0 ? 0 : minW; 35290 minH = 0; 35291 35292 self.items().each(function(item) { 35293 minW = Math.max(minW, item.layoutRect().minW); 35294 minH = Math.max(minH, item.layoutRect().minH); 35295 }); 35296 35297 self.items().each(function(ctrl) { 35298 ctrl.settings.x = 0; 35299 ctrl.settings.y = 0; 35300 ctrl.settings.w = minW; 35301 ctrl.settings.h = minH; 35302 35303 ctrl.layoutRect({ 35304 x: 0, 35305 y: 0, 35306 w: minW, 35307 h: minH 35308 }); 35309 }); 35310 35311 var headH = DomUtils.getSize(self.getEl('head')).height; 35312 35313 self.settings.minWidth = minW; 35314 self.settings.minHeight = minH + headH; 35315 35316 rect = self._super(); 35317 rect.deltaH += headH; 35318 rect.innerH = rect.h - rect.deltaH; 35319 35320 return rect; 35321 } 35322 }); 35323 }); 35324 35325 // Included from: js/tinymce/classes/ui/TextBox.js 35326 35327 /** 35328 * TextBox.js 35329 * 35330 * Copyright, Moxiecode Systems AB 35331 * Released under LGPL License. 35332 * 35333 * License: http://www.tinymce.com/license 35334 * Contributing: http://www.tinymce.com/contributing 35335 */ 35336 35337 /** 35338 * Creates a new textbox. 35339 * 35340 * @-x-less TextBox.less 35341 * @class tinymce.ui.TextBox 35342 * @extends tinymce.ui.Widget 35343 */ 35344 define("tinymce/ui/TextBox", [ 35345 "tinymce/ui/Widget", 35346 "tinymce/ui/DomUtils" 35347 ], function(Widget, DomUtils) { 35348 "use strict"; 35349 35350 return Widget.extend({ 35351 /** 35352 * Constructs a instance with the specified settings. 35353 * 35354 * @constructor 35355 * @param {Object} settings Name/value object with settings. 35356 * @setting {Boolean} multiline True if the textbox is a multiline control. 35357 * @setting {Number} maxLength Max length for the textbox. 35358 * @setting {Number} size Size of the textbox in characters. 35359 */ 35360 init: function(settings) { 35361 var self = this; 35362 35363 self._super(settings); 35364 35365 self._value = settings.value || ''; 35366 self.addClass('textbox'); 35367 35368 if (settings.multiline) { 35369 self.addClass('multiline'); 35370 } else { 35371 // TODO: Rework this 35372 self.on('keydown', function(e) { 35373 if (e.keyCode == 13) { 35374 self.parents().reverse().each(function(ctrl) { 35375 e.preventDefault(); 35376 35377 if (ctrl.hasEventListeners('submit') && ctrl.toJSON) { 35378 ctrl.fire('submit', {data: ctrl.toJSON()}); 35379 return false; 35380 } 35381 }); 35382 } 35383 }); 35384 } 35385 }, 35386 35387 /** 35388 * Getter/setter function for the disabled state. 35389 * 35390 * @method value 35391 * @param {Boolean} [state] State to be set. 35392 * @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation. 35393 */ 35394 disabled: function(state) { 35395 var self = this; 35396 35397 if (self._rendered && typeof(state) != 'undefined') { 35398 self.getEl().disabled = state; 35399 } 35400 35401 return self._super(state); 35402 }, 35403 35404 /** 35405 * Getter/setter function for the control value. 35406 * 35407 * @method value 35408 * @param {String} [value] Value to be set. 35409 * @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation. 35410 */ 35411 value: function(value) { 35412 var self = this; 35413 35414 if (typeof(value) != "undefined") { 35415 self._value = value; 35416 35417 if (self._rendered) { 35418 self.getEl().value = value; 35419 } 35420 35421 return self; 35422 } 35423 35424 if (self._rendered) { 35425 return self.getEl().value; 35426 } 35427 35428 return self._value; 35429 }, 35430 35431 /** 35432 * Repaints the control after a layout operation. 35433 * 35434 * @method repaint 35435 */ 35436 repaint: function() { 35437 var self = this, style, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect; 35438 35439 style = self.getEl().style; 35440 rect = self._layoutRect; 35441 lastRepaintRect = self._lastRepaintRect || {}; 35442 35443 // Detect old IE 7+8 add lineHeight to align caret vertically in the middle 35444 var doc = document; 35445 if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) { 35446 style.lineHeight = (rect.h - borderH) + 'px'; 35447 } 35448 35449 borderBox = self._borderBox; 35450 borderW = borderBox.left + borderBox.right + 8; 35451 borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0); 35452 35453 if (rect.x !== lastRepaintRect.x) { 35454 style.left = rect.x + 'px'; 35455 lastRepaintRect.x = rect.x; 35456 } 35457 35458 if (rect.y !== lastRepaintRect.y) { 35459 style.top = rect.y + 'px'; 35460 lastRepaintRect.y = rect.y; 35461 } 35462 35463 if (rect.w !== lastRepaintRect.w) { 35464 style.width = (rect.w - borderW) + 'px'; 35465 lastRepaintRect.w = rect.w; 35466 } 35467 35468 if (rect.h !== lastRepaintRect.h) { 35469 style.height = (rect.h - borderH) + 'px'; 35470 lastRepaintRect.h = rect.h; 35471 } 35472 35473 self._lastRepaintRect = lastRepaintRect; 35474 self.fire('repaint', {}, false); 35475 35476 return self; 35477 }, 35478 35479 /** 35480 * Renders the control as a HTML string. 35481 * 35482 * @method renderHtml 35483 * @return {String} HTML representing the control. 35484 */ 35485 renderHtml: function() { 35486 var self = this, id = self._id, settings = self.settings, value = self.encode(self._value, false), extraAttrs = ''; 35487 35488 if ("spellcheck" in settings) { 35489 extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; 35490 } 35491 35492 if (settings.maxLength) { 35493 extraAttrs += ' maxlength="' + settings.maxLength + '"'; 35494 } 35495 35496 if (settings.size) { 35497 extraAttrs += ' size="' + settings.size + '"'; 35498 } 35499 35500 if (settings.subtype) { 35501 extraAttrs += ' type="' + settings.subtype + '"'; 35502 } 35503 35504 if (self.disabled()) { 35505 extraAttrs += ' disabled="disabled"'; 35506 } 35507 35508 if (settings.multiline) { 35509 return ( 35510 '<textarea id="' + id + '" class="' + self.classes() + '" ' + 35511 (settings.rows ? ' rows="' + settings.rows + '"' : '') + 35512 ' hidefocus="1"' + extraAttrs + '>' + value + 35513 '</textarea>' 35514 ); 35515 } 35516 35517 return '<input id="' + id + '" class="' + self.classes() + '" value="' + value + '" hidefocus="1"' + extraAttrs + ' />'; 35518 }, 35519 35520 /** 35521 * Called after the control has been rendered. 35522 * 35523 * @method postRender 35524 */ 35525 postRender: function() { 35526 var self = this; 35527 35528 DomUtils.on(self.getEl(), 'change', function(e) { 35529 self.fire('change', e); 35530 }); 35531 35532 return self._super(); 35533 }, 35534 35535 remove: function() { 35536 DomUtils.off(this.getEl()); 35537 this._super(); 35538 } 35539 }); 35540 }); 35541 35542 // Included from: js/tinymce/classes/ui/Throbber.js 35543 35544 /** 35545 * Throbber.js 35546 * 35547 * Copyright, Moxiecode Systems AB 35548 * Released under LGPL License. 35549 * 35550 * License: http://www.tinymce.com/license 35551 * Contributing: http://www.tinymce.com/contributing 35552 */ 35553 35554 /** 35555 * This class enables you to display a Throbber for any element. 35556 * 35557 * @-x-less Throbber.less 35558 * @class tinymce.ui.Throbber 35559 */ 35560 define("tinymce/ui/Throbber", [ 35561 "tinymce/ui/DomUtils", 35562 "tinymce/ui/Control" 35563 ], function(DomUtils, Control) { 35564 "use strict"; 35565 35566 /** 35567 * Constructs a new throbber. 35568 * 35569 * @constructor 35570 * @param {Element} elm DOM Html element to display throbber in. 35571 * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll. 35572 */ 35573 return function(elm, inline) { 35574 var self = this, state, classPrefix = Control.classPrefix; 35575 35576 /** 35577 * Shows the throbber. 35578 * 35579 * @method show 35580 * @param {Number} [time] Time to wait before showing. 35581 * @return {tinymce.ui.Throbber} Current throbber instance. 35582 */ 35583 self.show = function(time) { 35584 self.hide(); 35585 35586 state = true; 35587 35588 window.setTimeout(function() { 35589 if (state) { 35590 elm.appendChild(DomUtils.createFragment( 35591 '<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>' 35592 )); 35593 } 35594 }, time || 0); 35595 35596 return self; 35597 }; 35598 35599 /** 35600 * Hides the throbber. 35601 * 35602 * @method hide 35603 * @return {tinymce.ui.Throbber} Current throbber instance. 35604 */ 35605 self.hide = function() { 35606 var child = elm.lastChild; 35607 35608 if (child && child.className.indexOf('throbber') != -1) { 35609 child.parentNode.removeChild(child); 35610 } 35611 35612 state = false; 35613 35614 return self; 35615 }; 35616 }; 35617 }); 35618 35619 expose(["tinymce/dom/EventUtils","tinymce/dom/Sizzle","tinymce/util/Tools","tinymce/Env","tinymce/dom/DomQuery","tinymce/html/Styles","tinymce/dom/TreeWalker","tinymce/dom/Range","tinymce/html/Entities","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/BookmarkManager","tinymce/dom/Selection","tinymce/dom/ElementUtils","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/ComboBox","tinymce/ui/ColorBox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/util/Color","tinymce/ui/ColorPicker","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); 35620 })(this);